SpringSecurity+JWT 构建用户认证和用户授权框架

说明:
SpringSecurity是一个用于Java 企业级应用程序的安全框架,主要包含用户认证和用户授权两个方面.相比较Shiro而言,Security功能更加的强大,它可以很容易地扩展以满足更多安全控制方面的需求,但也相对它的学习成本会更高,两种框架各有利弊.实际开发中还是要根据业务和项目的需求来决定使用哪一种.

JWT是在Web应用中安全传递信息的规范,从本质上来说是Token的演变,是一种生成加密用户身份信息的Token,特别适用于分布式单点登陆的场景,无需在服务端保存用户的认证信息,而是直接对Token进行校验获取用户信息,使单点登录更为简单灵活.

Maven相关依赖如下:

<!--Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

属性配置如下:

# JWT配置
# 密匙KEY
secret: 123456
# HeaderKEY
tokenHeader: Authorization
# Token前缀字符
tokenPrefix: fhpt-
# 过期时间 单位毫秒 20分钟
expiration: 7600000
#expiration: 40000
# 配置不需要认证的接口
antMatchers: /user/**,/dataSet/sampleExcel/download,/doc.html

JWTConfig :

@Component
public  class JWTConfig {
     
    @Value("${secret}")
    public String secret;
    @Value("${tokenHeader}")
    public String tokenHeader;
    @Value("${tokenPrefix}")
    public String tokenPrefix;
    @Value("${expiration}")
    public String expiration;
    @Value("${antMatchers}")
    public String antMatchers;
    public String getSecret() {
     
        return secret;
    }
    public void setSecret(String secret) {
     
        this.secret = secret;
    }
    public String getTokenHeader() {
     
        return tokenHeader;
    }
    public void setTokenHeader(String tokenHeader) {
     
        this.tokenHeader = tokenHeader;
    }
    public String getTokenPrefix() {
     
        return tokenPrefix;
    }
    public void setTokenPrefix(String tokenPrefix) {
     
        this.tokenPrefix = tokenPrefix;
    }
    public String getExpiration() {
     
        return expiration;
    }
    public void setExpiration(String expiration) {
     
        this.expiration = expiration;
    }
    public String getAntMatchers() {
     
        return antMatchers;
    }
    public void setAntMatchers(String antMatchers) {
     
        this.antMatchers = antMatchers;
    }
}

JWT工具类:

/**
 * JWT工具类
 */
@Slf4j
public class JWTTokenUtil {
     
    private static JWTConfig jwtConfig=null;
    public static JWTConfig getJWTConfig(){
     
        if(jwtConfig==null){
     
            jwtConfig = SpringContextUtil.getBeanByClass(JWTConfig.class);
        }
        return jwtConfig;
    }
    /**
     * 生成Token
     * @Param userInfo 用户安全实体
     * @Return Token
     */
    public static String createAccessToken(UserInfo userInfo){
     
        // 登陆成功生成JWT
        String expiration =getJWTConfig().getExpiration();
        Integer expirate =StringUtils.isEmpty(expiration)?86400:Integer.parseInt(expiration);
        log.info("创建token: userId:{}, userCode:{}",userInfo.getId(),userInfo.getUserCode());
        String token = Jwts.builder()
                // 放入用户名和用户ID
                .setId(userInfo.getId()+"")
                 // 主题
                .setSubject(userInfo.getUserCode())
                 // 签发时间,可选
                //.setIssuedAt(new Date())
                // 签发者,可选
                //.setIssuer("xie")
                .claim("groupId",userInfo.getGroupId())
                // 自定义属性 放入用户拥有权限
                .claim("authorities", JSON.toJSONString(userInfo.getAuthorities()))
                //.claim("groupId",)
                    // 失效时间
                .setExpiration(new Date(System.currentTimeMillis() + expirate))
                 // 签名算法和密钥,可选
                .signWith(SignatureAlgorithm.HS512, getJWTConfig().getSecret())
                .compact();
        return token;
    }
}

JWT接口请求校验拦截器:

 /**
 * JWT接口请求校验拦截器
 * 请求接口时会进入这里验证Token是否合法和过期
 * @Author xieGuanglin
 * @CreateTime 2020/5/12
 */
@Slf4j
public class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter {
     
    /**初始化配置类*/
    private JWTConfig jwtConfig=null;
    public JWTConfig getJWTConfig(){
     
        if(jwtConfig==null){
     
            jwtConfig = SpringContextUtil.getBeanByClass(JWTConfig.class);
        }
        return jwtConfig;
    }
    public JWTAuthenticationTokenFilter(AuthenticationManager authenticationManager) {
     
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
     
        //设置字符
        response.setCharacterEncoding("utf-8");
        String msg="登录过期,请重新登录";
        // 获取前端请求头中JWT的Token
        String tokenHeader = request.getHeader(getJWTConfig().getTokenHeader());
        if (tokenHeader!=null && tokenHeader.startsWith(getJWTConfig().getTokenPrefix())) {
     
            try {
     
                // 获取token
                String token = tokenHeader.replace(getJWTConfig().getTokenPrefix(), "");
                // 解析JWT中token
                Claims claims = Jwts.parser()
                        .setSigningKey(getJWTConfig().getSecret())
                        .parseClaimsJws(token)
                        .getBody();
                // 获取用户名
                String username = claims.getSubject();
                String userId=claims.getId();
                int groupId =(Integer) claims.get("groupId");
                log.info("获取token中的用户名:{},用户id:{}",username,userId);
                if(!StringUtils.isEmpty(username)&&!StringUtils.isEmpty(userId)) {
     
                    // 获取角色
                    List<GrantedAuthority> authorities = new ArrayList<>();
                    String authority = claims.get("authorities").toString();
                    if(!StringUtils.isEmpty(authority)){
     
                        List<Map<String,String>> authorityMap = JSONObject.parseObject(authority, List.class);
                        for(Map<String,String> role : authorityMap){
     
                            if(!StringUtils.isEmpty(role)) {
     
                                authorities.add(new SimpleGrantedAuthority(role.get("authority")));
                            }
                        }
                    }
                    //组装参数
                    UserInfo userInfo = new UserInfo();
                    userInfo.setUserCode(claims.getSubject());
                    userInfo.setId(Integer.parseInt(claims.getId()));
                    userInfo.setGroupId(groupId);
                    userInfo.setAuthorities(authorities);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userInfo, userId, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (ExpiredJwtException e){
     
                //log.info("Token过期");
                Result result =new Result(100,msg);
                log.info("响应前端:{}", JSON.toJSONString(result));
                response.getWriter().write(JSON.toJSONString(result));
                //终止方法, 否则框架自动重定向到login页面,不适用前后端分离系统
                return;

            }
            catch (Exception e) {
     
                //log.info("Token无效");
                Result result =new Result(100,msg);
                log.info("响应前端:{}", JSON.toJSONString(result));
                response.getWriter().write(JSON.toJSONString(result));
                return;
            }
        }
        filterChain.doFilter(request, response);
        return;
    }
}

自定义登录验证:

@Component
@Slf4j
public class UserAuthenticationProvider implements AuthenticationProvider {
     
    @Autowired
    private UserService userService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
     
        // 获取表单输入中返回的用户名, 前端post请求, 默认用户名参数: username
        String userCode =authentication.getName();
        //或log.info("用户名:{}", (String) authentication.getPrincipal());
        if(StringUtils.isEmpty(userCode)){
     
            throw new UsernameNotFoundException("请输入用户名");
        }
        // 获取表单中输入的密码
        String password = (String) authentication.getCredentials();
        // 查询用户是否存在
        UserEntity userEntity = userService.loginByUserCode(userCode);
        if (userEntity ==null ) {
     
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 我们还要判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的
        if(!MD5Util.getMD5Str(password).equals(userEntity.getPassword())){
     
            throw new BadCredentialsException("密码不正确");
        }
        /*if (!new BCryptPasswordEncoder().matches(password, userEntity.getPassword())) {
            throw new BadCredentialsException("密码不正确");
        }*/
        // 还可以加一些其他信息的判断,比如用户账号已停用等判断
        if (0==userEntity.getStatus()){
     
            throw new LockedException("该用户已被冻结");
        }
        UserInfo userInfo=new UserInfo();
        BeanUtils.copyProperties(userEntity, userInfo);
        Integer curGroupId 	=userService.getCurrentGroupId(userEntity.getId());
        userInfo.setGroupId(curGroupId);
        // 角色集合
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 查询用户角色
        List<RoleEntity> roleEntityList = userService.selectRoleById(userEntity.getId());
        for (RoleEntity roleEntity: roleEntityList){
     
            log.info("用户:{} 拥有权限:{}",userCode ,"ROLE_" + roleEntity.getRoleName());
            authorities.add(new SimpleGrantedAuthority("ROLE_" + roleEntity.getRoleName()));
        }
        userInfo.setAuthorities(authorities);
        // 进行登录
        return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
    }
    @Override
    public boolean supports(Class<?> authentication) {
     

        return true;
    }
}

暂无权限处理类:

@Component
@Slf4j
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
     
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
     
        log.info(httpServletRequest.getRequestURL().toString());
        Result result =new Result(100,"请登录");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

用户未登录处理类:

@Component
@Slf4j
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
     
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
     
        log.info(httpServletRequest.getRequestURL().toString());
        Result result =new Result(100,"请登录");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

登录成功处理类:

/**
 * @Description 登录成功处理类
 */
@Component
@Slf4j
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
     

    @Value("${secret}")
    private String secret;

    @Value("${tokenHeader}")
    private String tokenHeader;

    @Value("${tokenPrefix}")
    private String tokenPrefix;
    @Autowired
    private GroupRepository groupRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
     
        // 组装JWT
        UserInfo userInfo = (UserInfo) authentication.getPrincipal();
        String token = JWTTokenUtil.createAccessToken(userInfo);
        token = tokenPrefix + token;
        Integer groupId =userInfo.getGroupId();
        // 封装返回参数
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code",0);
        resultData.put("msg", "登录成功");
        resultData.put("groupId",groupId);
        resultData.put("token",token);
        httpServletResponse.setCharacterEncoding("utf-8");
      httpServletResponse.getWriter().write(JSON.toJSONString(resultData));
    }
}

登录失败处理类:

@Slf4j
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
     
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
     
        String msg =e.getMessage();
        Result result =new Result(500,e.getMessage());
        // log.info("返回异常信息:{}",e.getMessage());
        // 这些对于操作的处理类可以根据不同异常进行不同处理
        if (e instanceof UsernameNotFoundException){
     
            msg ="用户名不存在";
        }
        if (e instanceof LockedException){
     
            msg ="用户被冻结";
        }
        if (e instanceof BadCredentialsException){
     
            msg ="用户名密码不正确";
        }
        log.info("【登录失败】"+msg);
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

用户登出:

@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {
     
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
     
        SecurityContextHolder.clearContext();
        Result result =new Result(0,"登出成功");
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

自定义权限注解类:

@Slf4j
@Component
public class UserPermissionEvaluator implements PermissionEvaluator {
     
    @Autowired
    private UserService userService;
    /**
     * hasPermission鉴权方法
     * 这里仅仅判断PreAuthorize注解中的权限表达式
     * 实际中可以根据业务需求设计数据库通过targetUrl和permission做更复杂鉴权
     * 当然targetUrl不一定是URL可以是数据Id还可以是管理员标识等,这里根据需求自行设计
     * @Param authentication 用户身份(在使用hasPermission表达式时Authentication参数默认会自动带上)
     * @Param targetUrl 请求路径
     * @Param permission 请求路径权限
     * @Return boolean 是否通过
     */
    @SneakyThrows
    @Override
    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
     
        // 获取用户信息
        UserInfo userEntity =(UserInfo) authentication.getPrincipal();
        // 查询用户权限(这里可以将权限放入缓存中提升效率)
        Set<String> permissions = new HashSet<>();
        log.info("targetUrl:{},权限:{}",targetUrl,permission.toString());
        // 权限对比
        permissions.add("select");
        permissions.add("update");
        permissions.add("insert");
        boolean flag=false;
        if (permissions.contains(permission.toString())){
     
            return true;
        }

        log.info("用户:{}没有权限访问",userEntity.getUserCode());
        //SecurityContextHolder.getContext().getAuthentication().s
        return false;
    }
    @Override
    public boolean hasPermission(Authentication authentication, Serializable serializable, String s, Object o) {
     
        return false;
    }
}

无权限处理类:

@Component
@Slf4j
public class UserAuthAccessDeniedHandler extends Throwable implements AccessDeniedHandler {
     
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
     
        log.info("未授权操作:"+e.getMessage());
        Result result =new Result(403,"未授权");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

SpringSecurity核心配置类:

/**
 * SpringSecurity核心配置类
 */
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
     
    @Autowired
    private JWTConfig jWTConfig;
    /**
     * 自定义登录成功处理器
     */
    @Autowired
    private UserLoginSuccessHandler userLoginSuccessHandler;
    /**
     * 自定义登录失败处理器
     */
    @Autowired
    private UserLoginFailureHandler userLoginFailureHandler;
    /**
     * 自定义注销成功处理器
     */
    @Autowired
    private UserLogoutSuccessHandler userLogoutSuccessHandler;
    /**
     * 自定义暂无权限处理器
     */
    @Autowired
    private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
    /**
     * 自定义未登录的处理器
     */
    @Autowired
    private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
    /**
     * 自定义登录逻辑验证器
     */
    @Autowired
    private UserAuthenticationProvider userAuthenticationProvider;
    /**
     * 加密方式
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
     
        return new BCryptPasswordEncoder();
    }
    /**
     * 注入自定义PermissionEvaluator
     */
    @Bean
    public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
     
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new UserPermissionEvaluator());
        return handler;
    }
    /**
     * 配置登录验证逻辑
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth){
     
        //这里可启用我们自己的登陆验证逻辑
        auth.authenticationProvider(userAuthenticationProvider);
    }
    /**
     * 配置security的控制逻辑
     * @Param http 请求
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
     
        http.authorizeRequests()
                //不进行权限验证的请求或资源(从配置文件中读取)
                .antMatchers(jWTConfig.antMatchers.split(",")).permitAll()
                //其他的需要登陆后才能访问
                .anyRequest().authenticated()
                .and()
                //配置未登录自定义处理类
                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
                .and()
                //配置登录地址
                .formLogin()
                .loginProcessingUrl("/user/login")
                //配置登录成功自定义处理类
                .successHandler(userLoginSuccessHandler)
                //配置登录失败自定义处理类
                .failureHandler(userLoginFailureHandler)
                .and()
                //配置登出地址
                .logout()
                .logoutUrl("/user/logout")
                //配置用户登出自定义处理类
                .logoutSuccessHandler(userLogoutSuccessHandler)
                .and()
                //配置没有权限自定义处理类
                .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
                .and()
                // 开启跨域
                .cors()
                .and()
                // 取消跨站请求伪造防护
                .csrf().disable();
        // 基于Token不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 禁用缓存
        http.headers().cacheControl();
        // 添加JWT过滤器
        http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
    }
}

最后控制层controller调用:

	//方法上加权限注解
	//拥有ADMIN
	@PreAuthorize("hasRole('ADMIN')") 
	//拥有ADMIN或者USER角色可以访问
	@PreAuthorize("hasAnyRole('ADMIN','USER')")
	//拥有ADMIN和USER角色可以访问
	@PreAuthorize("hasRole('ADMIN') and hasRole('USER')")
	//拥有select权限可以访问
    //hasPermission 第一个参数是请求路径 第二个参数是权限表达式
    @PreAuthorize("hasPermission('/admin/userList','select')")
    /*@PreAuthorize("hasRole('ADMIN') and hasPermission('/modelInfo/view','select')")*/
    //拥有ADMIN角色和update权限可以访问
    @PreAuthorize("hasRole('ADMIN') and hasPermission('/modelInfo/update','update')")
    @GetMapping("/view")
    public Result view(@RequestParam(value = "id") long id) {
     
        Result result = new Result<>();
        return result;
    }

最后欢迎大家访问和使用我的个人网站:助记宝

你可能感兴趣的:(SpringBoot)