SpringSecurity做JWT认证授权详解

第一、Spring Security简介

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

第二、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包含的信息尽可能的简洁。

第四、 设计流程


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

 第五、实现过程---引入pom依赖

<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);

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 

@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {

    @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 

@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

@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 

@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();
    }

}

 实现登录方法

    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接口 

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

测试Token是否能正常使用 

UserController.java

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

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

 持续学习!!!

猜你喜欢

转载自blog.csdn.net/SHYLOGO/article/details/105240336