美文网首页technology-integration全面解析程序员
technology-integration(七)---使用Sp

technology-integration(七)---使用Sp

作者: 海苔胖胖 | 来源:发表于2018-08-27 18:47 被阅读1334次

    SpringSecurity是什么

    Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。(引用百度百科)

    为什么使用SpringSecurity

    可以说SpringBoot的快速发展也使得了SpringSecurity的热度往上涨,在SpringBoot以前,Shiro和SpringSecurity的框架都是主流的安全框架,但Security的配置非常臃肿,性能也相对慢一些,最大的缺点还是必须依赖于Spring框架才能开发,不过唯一值得一提的就是Security默认实现了更多功能,更是提供了oauth授权的实现。不过一切的改变还是源于SpringBoot的出现,SpringBoot整合SpringSecurity的步骤非常简单,只需要继承WebSecurityConfigurerAdapter这个类并实现认证方法,接着配置一下登录的uri即可完成一个简单的用户认证功能,可以说整个功能都不用5分钟就能实现。我个人还是比较喜欢SpringBoot全家桶,不需要解决框架整合上的小麻烦,功能上来说也很强大。

    JWT

    JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库

    怎么使用

    使用JWT,我们只需要在请求的请求头上添加如图下类似的数据(token)。后端根据需要认证的url进行拦截,取出Hearders里面的数据,紧接着解析出这段token的包含的信息,判断信息是否正确即可。token其实就是根据信息加密而来的一段字符串,我们将需要用到的信息放到token中,token包含的信息尽可能的简洁。


    image.png

    注意:

    虽然简单的jwt认证并没有什么难度,但如果你没使用过SpringSecurity,建议还是先去简单的学习一下。


    开始

    1. 编写通过用户id或用户手机号码查询User和Role的方法
    2. 编写Token生成工具类
    3. 继承UserDetails接口
    4. 继承UserDetailsService接口,实现用户认证方法
    5. 编写用户账号验证失败处理器与权限不足处理器
    6. 编写Token验证过滤器
    7. 配置SpringSecurity Config
    8. 实现登录方法

    整个流程还是相对完善的,所以步骤稍多

    导入jar

    这里需要提一下的就是,当你引入这个包的时候,SpringBoot默认会为项目所有的请求添加认证,这也是SpringSecurity的常规操作,如果你还不知道的话,赶快刹车调头回家补课。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    

    实现用户登录方法

    用户通过手机号及密码进行登录,我们需要先获取用户的身份信息以及角色信息

    UserMapper.xml
      <resultMap id="User_Role" type="com.viu.technology.po.User">
        <id property="id" column="id" javaType="java.lang.String" jdbcType="BIGINT" />
        <result property="name" column="name" javaType="java.lang.String" jdbcType="VARCHAR" />
        <result property="phone" column="phone" javaType="java.lang.String" jdbcType="VARCHAR" />
        <result property="password" column="password" javaType="java.lang.String" jdbcType="VARCHAR" />
        <collection property="roles" ofType="com.viu.technology.po.Role">
          <id property="id" column="role_id" jdbcType="BIGINT"/>
          <result property="roleName" column="role_name" jdbcType="VARCHAR" />
        </collection>
      </resultMap>
    
      <select id="selUserAndRoleByPhone" parameterType="java.lang.String" resultMap="User_Role">
        SELECT u.id,u.name,u.password,u.phone,r.id as role_id ,r.role_name
        from t_user u
        LEFT JOIN t_role r on u.id=r.user_id
        where u.phone=#{phone,jdbcType=VARCHAR}
      </select>
    
      <select id="selUserAndRoleById" parameterType="java.lang.String" resultMap="User_Role">
        SELECT u.id,u.name,u.password,u.phone,r.id as role_id ,r.role_name
        from t_user u
        LEFT JOIN t_role r on u.id=r.user_id
        where u.id=#{id,jdbcType=VARCHAR}
      </select>
    
    UserMapper.java
        User selUserAndRoleByPhone(String phone);
    
        User selUserAndRoleById(String id);
    
    UserDao.java
        User selUserAndRoleByPhone(String phone);
    
        User selUserAndRoleById(String id);
    
    UserDaoImpl.java
        public User selUserAndRoleByPhone(String phone) {
            User user = userMapper.selUserAndRoleByPhone(phone);
            return user;
        }
    
    
        public User selUserAndRoleById(String id){
            User user = userMapper.selUserAndRoleById(id);
            return user;
        }
    
    UserService.java
        User getUserAndRoleByPhone(String phone);
    
        User getUserAndRoleById(String id);
    
    UserServiceImpl.java
        public User getUserAndRoleByPhone(String phone) {
            User user = userDao.selUserAndRoleByPhone(phone);
            return user;
        }
    
        public User getUserAndRoleById(String id) {
            User user = userDao.selUserAndRoleById(id);
            return user;
        }
    

    操作数据库获取用户身份信息的代码就到此为止了,接下来就开始编写SpringSecurity+jwt的认证代码了


    编写Token生成工具类----JwtTokenUtil

    工具类主要用作生成token、刷新token以及验证token。Token和Session一个很大的区别就是无登录状态,我们可以利用清除session做登出的操作,但无法利用token直接做登出操作,后续会进行讲解。
    这个token里的信息比较简单,只存放了sub和create,你可以根据自己业务需求在generateToken(UserDetails userDetails)方法里面添加不同的数据即可,后续通过getClaimsFromToken方法获取Claims对象,接着调用Claims对象的get方法获取出对应的数据即可。

    @Component
    public class JwtTokenUtil{
    
        /**
         * 密钥
         */
        private static final String secret = "lkhouhubkljgpihojblkjboiboihu9u";
    
        /**
         * 从数据声明生成令牌
         *
         * @param claims 数据声明
         * @return 令牌
         */
        public static String generateToken(Map<String, Object> claims) {
            //设置token的有效期为24*7小时,也就是一周
            Date expirationDate = new Date(System.currentTimeMillis() +60*60*24*7 * 1000);
            return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
        }
    
        /**
         * 从令牌中获取数据声明
         *
         * @param token 令牌
         * @return 数据声明
         */
        public static Claims getClaimsFromToken(String token) {
            Claims claims;
            try {
                claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
            } catch (Exception e) {
                claims = null;
            }
            return claims;
        }
    
        /**
         * 生成令牌
         *
         * @param userDetails 用户
         * @return 令牌
         */
        public static String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>(2);
            claims.put("sub", userDetails.getUsername());
            claims.put("created", new Date());
            return generateToken(claims);
        }
    
        /**
         * 从令牌中获取用户名
         *
         * @param token 令牌
         * @return 用户名
         */
        public static String getUsernameFromToken(String token) {
            String username;
            try {
                Claims claims = getClaimsFromToken(token);
                username = claims.getSubject();
            } catch (Exception e) {
                username = null;
            }
            return username;
        }
    
        /**
         * 判断令牌是否过期
         *
         * @param token 令牌
         * @return 是否过期
         */
        public static Boolean isTokenExpired(String token) {
            try {
                Claims claims = getClaimsFromToken(token);
                Date expiration = claims.getExpiration();
                return expiration.before(new Date());
            } catch (Exception e) {
                return false;
            }
        }
    
        /**
         * 刷新令牌
         *
         * @param token 原令牌
         * @return 新令牌
         */
        public static String refreshToken(String token) {
            String refreshedToken;
            try {
                Claims claims = getClaimsFromToken(token);
                claims.put("created", new Date());
                refreshedToken = generateToken(claims);
            } catch (Exception e) {
                refreshedToken = null;
            }
            return refreshedToken;
        }
    
        /**
         * 验证令牌
         *
         * @param token       令牌
         * @param userDetails 用户
         * @return 是否有效
         */
        public static Boolean validateToken(String token, UserDetails userDetails) {
            String username = getUsernameFromToken(token);
            return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
        }
    }
    

    继承UserDetails接口

    UserDetails接口是SpringSecurity框架用于认证授权的一个载体,只有实现了这个接口的类才能被SpringSecurity验证,

    public class User implements UserDetails {
        private String id;
    
        private String name;
    
        private String password;
    
        private String phone;
    
        private List<Role> roles;
    
    
        public List<Role> getRoles() {
            return roles;
        }
    
        public void setRoles(List<Role> roles) {
            this.roles = roles;
        }
    
        public User(String id, String name, String password, String phone) {
            this.id = id;
            this.name = name;
            this.password = password;
            this.phone = phone;
        }
    
        public User() {
            super();
        }
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        //获取用户角色权限,此处从数据库表Role中获取
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<GrantedAuthority> auths = new ArrayList<>();
            List<Role> roles = getRoles();
            if (roles!=null) {
                for (Role role : roles) {
                    auths.add(new SimpleGrantedAuthority(role.getRoleName()));
                }
            }
            return auths;
        }
    
        //这个是UserDetails默认实现获取密码的方法
        @Override
        public String getPassword() {
            return password;
        }
    
    
        //这里getUsername翻译过来就是获取用户名的意思,但这个可以作为我们获取用户信息的一个标识
        @Override
        public String getUsername() {
            return id;
        }
    
        //用户账号是否过期,暂时没这个功能,默认返回true,即未过期
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        //用户账号是否锁定
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        //用户凭证是否过期
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        //账号是否可用
        @Override
        public boolean isEnabled() {
            return true;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public String getPhone() {
            return phone;
        }
    
        public void setPhone(String phone) {
            this.phone = phone;
        }
    }
    

    编写登录认证方法JwtUserDetailsServiceImpl.java

    该类位于com.viu.technology.service.auth包下(自行建包)
    JwtUserDetailsServiceImpl实现了UserDetailsService接口,SpringSecurity会去IOC容器中寻找实现这个接口的实现类,并将该实现类作为默认的认证类。这个类主要用于获取用户身份信息,并不需要我们去判断用户名和密码是否匹配。参照UserDetails实现的getPassword和getUsername方法。

    这里之所要对username的长度进行判断是因为,我们登录的时候用的是手机号+明文密码进行登录,而保存在token里的信息只有id。登录方法和Token认证过滤器都会调用loadUserByUsername方法,所以需要做一个判断。可能会有一点疑问,既然是这样,为什么不直接用手机号做为token的传递信息就好了呢,主要还是因为我们使用手机号查询的情况比较少,而表的主键id才是经常用的。

    @Service
    public class JwtUserDetailsServiceImpl implements UserDetailsService {
        @Autowired
        @Lazy
        private UserService userService;
    
        public JwtUserDetailsServiceImpl(){
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = null;
            if (username.length() == 32) {
                user= userService.getUserAndRoleById(username);
            } else if(username.length()==11) {
                user= userService.getUserAndRoleByPhone(username);
            }
    
            log.info("user:" + user);
    
            if (user == null) {
                throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
            }else{
                return user;
            }
        }
    }
    

    编写账号密码验证失败处理器EntryPointUnauthorizedHandler.java

    位于com.viu.technology.handler包下,自行创建

    @Component
    public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
    
        private static Logger log = LoggerFactory.getLogger(EntryPointUnauthorizedHandler.class);
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setStatus(401);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(JsonUtil.objectToString(Result.fail(ResultCode.USER_LOGIN_FIAL)));
    
        }
    
    }
    

    编写账户权限不足处理器RestAccessDeniedHandler.java

    位于com.viu.technology.handler包下,自行创建

    @Component
    public class RestAccessDeniedHandler implements AccessDeniedHandler {
    
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setStatus(403);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(JsonUtil.objectToString(Result.fail(ResultCode.USER_PERMISSION_DENIED)));
        }
    }
    
    

    编写Token验证过滤器JwtAuthenticationTokenFilter.java

    位于com.viu.technology.filter包下,自行创建

    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        private static Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    
            String authHeader = request.getHeader("Authorization");
            //该字符串作为Authorization请求头的值的前缀
            String tokenHead = "tech-";
            if (authHeader != null && authHeader.startsWith(tokenHead)) {
                String authToken = authHeader.substring(tokenHead.length());
                //从token中获取userId
                String userId = JwtTokenUtil.getUsernameFromToken(authToken);
                if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    //调用UserDetailsService的认证方法(JwtUserDetailsServiceImpl实现类)
                    UserDetails userDetails = this.userDetailsService.loadUserByUsername(userId);
                    //验证token是否正确
                    if (JwtTokenUtil.validateToken(authToken, userDetails)) {
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        //将获取到的用户身份信息放到SecurityContextHolder中,这个类是为了在线程中保存当前用户的身份信息
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            } else {
                log.info("没有获取到token");
            }
            chain.doFilter(request, response);
        }
    
    
    
    }
    
    

    配置SpringSecurity

    位于com.viu.technology.config.security包下,自行创建

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)//开启security方法级别权限控制注解
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsService;
        @Autowired
        private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
        @Autowired
        private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
        @Autowired
        private RestAccessDeniedHandler restAccessDeniedHandler;
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private SimpleUrlAuthenticationSuccessHandler successHandler;
    
        @Autowired
        private SimpleUrlAuthenticationFailureHandler failureHandler;
    
        @Bean
        @Override
        protected AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity
                    .csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    
                    .and()
                    .authorizeRequests()
                    //这里的参数为不需要认证的uri,**代表匹配多级路径,*代表匹配一级路径,#代表一个字符....
                    .antMatchers(
                            "/demo/**",
                            "/user/generate/token"
                    ).permitAll()
                    //这里表示该路径需要管理员角色
                    .antMatchers("/auth/test").hasAnyAuthority("管理员")
                    .anyRequest().authenticated()
                    .and()
                    .headers().cacheControl();
    
    
            //添加认证过滤
            httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
            //添加权限不足及验证失败处理器
            httpSecurity.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);
    
        }
    
    
        //这个为SpringSecurity的加密类
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
    }
    

    实现登录方法

    UserService.java
        String login(String phone, String password);
    
    UserServiceImpl.java

    这里需要注意一下,UsernamePasswordAuthenticationToken会自动将password进行加密之后再比对,而我们之前写的注册用户方法是以明文方式存入数据库的,并没有加密,所以我们需要修改一下用户注册方法,然后重新注册

        public String login(String phone, String password) {
            //将用户名和密码生成Token
            UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(phone, password);
            //调用该方法时SpringSecurity会去调用JwtUserDetailsServiceImpl 进行验证
            Authentication authentication = authenticationManager.authenticate(upToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            return JwtTokenUtil.generateToken(userDetails);
        }
    
    
    
        @Autowired
        PasswordEncoder passwordEncoder;
    
        public User registerUser(User user) {
            //在插入数据库时将原密码进行加密
            user.setPassword(passwordEncoder.encode(user.getPassword()));
            User userRes = userDao.insertUser(user);
            Role roleRes = roleDao.insertRole(new Role("普通群众", user.getId()));
            List list = new ArrayList();
            list.add(roleRes);
    
            if (null != userRes && null != roleRes) {
                userRes.setRoles(list);
                return user;
            }
            return null;
        }
    
    UserController.java
        @PostMapping(value = "/generate/token")
        public Result getToken(String phone, String password) throws AuthenticationException {
    
            String token = userService.login(phone, password);
            return Result.success(token);
        }
    

    测试获取token接口

    获取token

    接着我们调用一下之前写的注册接口,发现没发注册,因为我们在SpringSecurity的配置中并没有开放这个接口的认证,自行添加。注册是不需要用户身份验证的,否则你让人家怎么注册嘛。。。


    image.png

    测试Token是否能正常使用

    UserController.java
        @GetMapping("/self/info")
        public Result getUserSelfInfo() {
           //由于通过验证后我们会把用户对象存到SecurityContextHolder中,所以这时候我们能通过下面这句代码获取到用户的身份信息
            User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            return Result.success(user);
        }
    

    接下来测试一下,如果能够正常获取就代表成功,记住token前面要加tech-这个几个字符串,看不顺眼的话自己去改过滤器



    温馨提醒

    你们会发现读出来的数据和我稍微有点不一样对吧,哈哈哈哈,肯定啊,你们没有过滤一下敏感字段(密码我忘了过滤了0.0),在User类上加入@JSONField(serialize = false)注解即可,SpringBoot会将持有该注解的字段过滤不进行输出。


    image.png

    更多文章请关注该 technology-integration全面解析专题

    相关文章

      网友评论

        本文标题:technology-integration(七)---使用Sp

        本文链接:https://www.haomeiwen.com/subject/klftwftx.html