SpringSecurity和JWT实现认证和授权

SpringSecurity

SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标签。SpringSecurity注重于Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。

JWT

JWT是JSON WEB TOKEN的缩写,它是基于RFC 7519标准定义的一种可以安全传输的JSON对象,由于使用了数字签名,所以是可信任和安全的。

JWT的组成

JWT token的格式:header.payload.singatue

  1. head中用于存放签名的生成算法
    {"alg":"HS512"}
  2. payload中用于存放用户名、token的生成时间和过期时间
    {"sub":"admin","created":1489079981339,"exp":1489684155}
  3. signature为以header和payload生产的签名,一旦header和payload被篡改,验证将失败
    {"sub":"admin","created":1489079981339,"exp":1489684155}

JWT实现认证和授权的原理

  1. 用户调用登录接口,登录成功后获取JWT的Token
  2. 之后用户每次调用接口都在http的header中添加一个叫Authorization的头,值为JWT的token
  3. 后台程序通过对Authorization头中信息的解码及数字签名校验来获取其中的用户信息,从而实现认证和授权。

JWT的工作流程

  1. 用户导航到登录页面,输入用户名和密码进行登录
  2. 服务器在验证登录授权,如果用户合法,则会根据用户的信息和服务器所定义好的规则生成JWT Token
  3. 服务器将该Token以JSON的格式进行返回
  4. 用户得到Token后,存储在localStorage或者以其它的数据格式存储
  5. 以后用户每次请求时,需要在请求的header中加入Authorization:Bearer token
  6. 服务器对Token进行校验,合法就解析其中的内容,然后根据拥有的权限和业务逻辑给出对应的响应结果
  7. 用户注销后,令牌将在客户端进行销毁,不需要与服务器进行交互。

Authentication和Authorization的区别:

Authentication:用户认证,应用程序通过帐号和密码确认用户

Authorization:授权,在用户确认身份之后提供权限,比如管理员可以修改数据,其它用户只允许阅读数据

基于SpringSecurity与JWT Token的使用

下载地址: https://gitee.com/zhou_yunong/mall-learning.git

项目使用表说明

  1. ums_admin:后台用户表
  2. ums_role:后台用户角色表
  3. ums_permission:后台用户权限表
  4. ums_admin_role_relation:后台用户和角色关系表,用户与角色是多对多关系
  5. ums_role_permission_relation:后台用户角色和权限关系表,角色与权限是多对多关系
  6. ums_admin_permission_relation:后台用户和权限关系表(除角色中定义的权限以外的加减权限),加权限是指用户比角色多出的权限,减权限是指用户比角色少的权限

所需要的表说明

  1. JwtTokenUtil(JWT Token生成工具类)
@Component
public class JwtTokenUtil {
    //定义Log4J文件
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";


    //JWT密钥
    @Value("${jwt.secret}")
    private String secret;

    //JWT的过期时间
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 根据负责生成JWT的Token
     */
    private String  generateToken(Map<String,Object> claims) {
        return Jwts.builder()
                .setClaims(claims)                                  //自定义属性
                .setExpiration(generateExpirationDate())            //过期时间
                .signWith(SignatureAlgorithm.HS512,secret)          //签名算法和密匙
                .compact();
    }

    /**
     * 从Token中获取到JWT的负载,解析JWT
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        //相当于一个Map,包含了我们所需要的信息
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    //设置签名的密钥
                    .setSigningKey(secret)
                    //设置需要解析的Token
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}",token);
        }
        /**
         * sub(用户名)、created(创建时间、exp(过期时间)
         * 解析结果:
         *  sub:admin
         *  created:1696877941
         *  exp:1482169527
         */
        return claims;
    }

    /**
     * 生成Token的过期时间
     * @return
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 从token当中获取登录的用户名
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token) {
        String userName;
        try {
            Claims claims = getClaimsFromToken(token);
            userName = claims.getSubject();
        } catch (Exception e) {
            userName = null;
        }
        return userName;
    }

    /**
     * 验证Token是否还有效
     * @param token         客户端传递过来的Token
     * @param userDetails   从数据库中查询用户的信息
     * @return
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        String userName = getUserNameFromToken(token);
        return userName.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断token是否失效
     * @param token
     * @return
     */
    private Boolean isTokenExpired(String token) {
        Date expireDate = getExpiredDateFromToken(token);
        //Token有效期小于当前时间返回TRUE(未过期),大于等于返回FALSE(已过期)
        return expireDate.before(new Date());
    }

    /**
     * 从Token当中获取过期的时间
     * @param token
     * @return
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 根据用户信息生成Token
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails) {
        Map<String,Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED,new Date());
        return generateToken(claims);
    }


    /**
     * 判断token是否可以被刷新
     */
    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     * token刷新
     * @param token
     * @return
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED,new Date());
        return generateToken(claims);
    }
}
  1. SpringSecurity的配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UmsAdminService umsAdminService;

    //当用户没有访问权限时的处理器,用于返回JSON格式的处理结果
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;

    //当未登录或Token失效时,返回JSON格式的结果
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    /**
     * 用于配置需要拦截的URL路径、JWT过滤器及出异常后的处理器
     * @param httpSecurity
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //由于使用的是JWT,所以不需要csrf跨域
        httpSecurity.csrf()
                .disable()
                //基于Token,因为不需要Session
                .sessionManagement()
                //SessionCreationPolicy用于管理Session的创建策略
                //SpringSecurity永远不创建Session,不使用Session获取SecurityContext
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //用于授权,可开放什么路径,什么资源
                .authorizeRequests()
                .antMatchers(HttpMethod.GET,    //允许对于网站静态资源的无授权访问
                        "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/swagger-resources/**",
                        "/v2/api-docs/**"
                )
                .permitAll()
                //登录注册允许匿名访问,不做权限
                .antMatchers("/admin/login","/admin/register")
                .permitAll()
                //在跨域请求会先进行一次options请求(开放)
                .antMatchers(HttpMethod.OPTIONS)
                .permitAll()
                //测试时全部运行访问
//                .antMatchers("/**")
//                .permitAll()
                //除了上面的所有请求均需要授权认证
                .anyRequest()
                .authenticated();
        //禁用缓存
        httpSecurity.headers().cacheControl();
        //添加JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定义未授权和未登录结果返回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);

    }

    /**
     * 用于配置UserDetailService和PasswordEncode
     * @param auth
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Bean
    public UserDetailsService userDetailsService() {
        //获取登录的用户信息
        return username -> {
            UmsAdmin admin = umsAdminService.getAdminByUsername(username);
            if (admin != null) {
                List<UmsPermission> permissionList = umsAdminService.getPermissionList(admin.getId());
                return new AdminUserDetails(admin,permissionList);
            }
            throw new UsernameNotFoundException("用户名或者密码输入错误");
        };
    }

    /**
     * 在用户名和密码校验前添加的过滤器,如果有jwt的Token,会自动根据token信息进行登录
     * @return
     */
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
  1. JwtAuthenticationTokenFilter(JWT登录授权器,每次登录都会请求这个类)
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;


    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        //获取Bearer -- Token
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            log.info("checking username:{}",username);
            //当token的username不为空是进行验证token是否为有效的token
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                //从数据库得到带有密码完整的user信息
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                //判断Token是否有效
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    log.info("authenticated user:{}",username);
                    //设置用户登录状态
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        chain.doFilter(request,response);
    }
}
  1. RestAuthenticationEntryPoint(未登录或Token访问失效时,自定义返回结果)
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
        response.getWriter().flush();
    }
}
  1. RestfulAccessDeniedHandler(当所访问的接口没有权限时,自定义返回结果)
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
        response.getWriter().flush();
    }
}
  1. AdminUserDetails(从Security获取用户数据)
public class AdminUserDetails implements UserDetails {
    private UmsAdmin umsAdmin;
    private List<UmsPermission> permissionList;
    public AdminUserDetails(UmsAdmin umsAdmin, List<UmsPermission> permissionList) {
        this.umsAdmin = umsAdmin;
        this.permissionList = permissionList;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //返回当前用户的权限
        return permissionList.stream()
                .filter(permission -> permission.getValue()!=null)
                .map(permission ->new SimpleGrantedAuthority(permission.getValue()))
                .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return umsAdmin.getPassword();
    }

    @Override
    public String getUsername() {
        return umsAdmin.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return umsAdmin.getStatus().equals(1);
    }

}
  1. Application.yml(自定义JWT的Key)
#自定义jwt Key
jwt:
  tokenHeader: Authorization  #JWT存储的请求头
  secret: mySecret            #JWT加解密所使用的密钥
  expiration: 604800          #JWT的超期限时间(60*60*24)
  tokenHead:  Bearer          #JWT负载中拿到开头
  1. Swagger2配置(自动记住Authorization令牌)
/**
 * Swagger2API文档的配置
 */
@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket createRestApi(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                //为当前包下controller生成API文档
                .apis(RequestHandlerSelectors.basePackage("com.mall.tiny.controller"))
                //为有@Api注解的Controller生成API文档
//                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                //为有@ApiOperation注解的方法生成API文档
//                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build()
                //添加登录认证
                .securitySchemes(securitySchemes())
                .securityContexts(securityContexts());
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("SwaggerUI演示")
                .description("mall-tiny")
                .contact("macro")
                .version("1.0")
                .build();
    }

    private List<ApiKey> securitySchemes() {
        //设置请求头信息
        List<ApiKey> result = new ArrayList<>();
        ApiKey apiKey = new ApiKey("Authorization", "Authorization","header");
        result.add(apiKey);
        return result;
    }

    private List<SecurityContext> securityContexts() {
        //设置需要登录认证的路径
        List<SecurityContext> result = new ArrayList<>();
        result.add(getContextByPath("/brand/.*"));
        return result;
    }

    private SecurityContext getContextByPath(String pathRegex){
        return SecurityContext.builder()
                .securityReferences(defaultAuth())
                .forPaths(PathSelectors.regex(pathRegex))
                .build();
    }

    private List<SecurityReference> defaultAuth() {
        List<SecurityReference> result = new ArrayList<>();
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        result.add(new SecurityReference("Authorization", authorizationScopes));
        return result;
    }
}
  1. 给需要调用的接口添加访问权限
    1、给查询接口添加pms:brand:read权限
    2、给修改接口添加pms:brand:update权限
    3、给删除接口添加pms:brand:delete权限
    4、给添加接口添加pms:brand:create权限
@PreAuthorize("hasAuthority('pms:brand:read')")

认证与授权流程演示地址和所作笔记来源

链接: mall-learing.

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值