SpringSecurity+jwt权限管理demo

一,前言

我发现很多开源项目在权限安全这方面用的都是Spring-Security。所以这段时间学习了 Spring-Security ,做了一个权限管理demo巩固一下。之前有做过一个 shiro 的权限管理demo,前端就是借用那个项目的,只有一点地方改变。shiro

二,代码实践

1,POM

不止有这几个依赖,后面会提供源码

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

        <!--Hutool Java工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.7</version>
        </dependency>
        <!--JWT(Json Web Token)登录支持-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

2,JwtTokenUtil

Jwt 工具类,用于生成token,验证token等操作…

@Component
public class JwtTokenUtil {

    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";
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    /**
     * 根据负责生成JWT的token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从token中获取JWT中的负载
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}",token);
        }
        return claims;
    }

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

    /**
     * 从token中获取登录用户名
     */
    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 从数据库中查询出来的用户信息
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

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

    /**
     * 根据用户信息生成token
     */
    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 oldToken 带tokenHead的token
     */
    public String refreshHeadToken(String oldToken) {
        if(StrUtil.isEmpty(oldToken)){
            return null;
        }
        String token = oldToken.substring(tokenHead.length());
        if(StrUtil.isEmpty(token)){
            return null;
        }
        //token校验不通过
        Claims claims = getClaimsFromToken(token);
        if(claims==null){
            return null;
        }
        //如果token已经过期,不支持刷新
        if(isTokenExpired(token)){
            return null;
        }
        //如果token在30分钟之内刚刷新过,返回原token
        if(tokenRefreshJustBefore(token,30*60)){
            return token;
        }else{
            claims.put(CLAIM_KEY_CREATED, new Date());
            return generateToken(claims);
        }
    }

    /**
     * 判断token在指定时间内是否刚刚刷新过
     * @param token 原token
     * @param time 指定时间(秒)
     */
    private boolean tokenRefreshJustBefore(String token, int time) {
        Claims claims = getClaimsFromToken(token);
        Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
        Date refreshDate = new Date();
        //刷新时间在创建时间的指定时间内
        if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){
            return true;
        }
        return false;
    }

}

补充

在token这部分内容我的实现思路是:
前端发起登录请求,登录成功获取token,这个token有效期为7天(可能太长了不合理)。
前端每隔一段时间刷新这个token,如果token过期,则重新登录。

//定时刷新token
const timer = setInterval(() => {
	let token = getToken()
	if (token == null || token == '') {
		return
	}
	store
		.dispatch('refreshToken')
		.then(() => {
			console.log('token刷新成功')
		})
		.catch(() => {
			console.log('token刷新失败')
		})
}, 60 * 1000)

3,AdminUserDetails

了解 Spring-Security的知道,框架提供了一个实现UserDetails接口的对象–User。这个可以说是认证授权的主体,即用户本身,存储着用户的权限,角色等信息。
我们可以自定义这个主体对象,从数据库综查询出用户的权限,角色等信息…只需要实现UserDetails接口即可

public class AdminUserDetails implements UserDetails {

    private Admin admin;

    private List<Resource> permissionList;

    public AdminUserDetails(Admin admin, List<Resource> permissionList) {
        this.admin = admin;
        this.permissionList = permissionList;
    }

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


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

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

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


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


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


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

4,JwtAuthenticationTokenFilter

在这个demo中,我们依然是从请求中获取前端发过来的token,然后进行一系列的验证。

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);

    @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 {
        //获取请求头中的token
        String authHeader = request.getHeader(this.tokenHeader);
        //判断token是否存在
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("checking username:{}", username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                //验证token是否还有效
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    LOGGER.info("authenticated user:{}", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

5,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(RestResponse.fail(SystemCode.AccessTokenError.getCode(),SystemCode.AccessTokenError.getMessage())));
        response.getWriter().flush();
    }
}

6,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(RestResponse.fail(SystemCode.AccessDenied.getCode(),SystemCode.AccessDenied.getMessage())));
        response.getWriter().flush();
    }
}

7,NRSCLogoutSuccessHandler

退出登录成功后返回结果

public class NRSCLogoutSuccessHandler implements LogoutSuccessHandler {

    private ObjectMapper objectMapper = new ObjectMapper();


    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(RestResponse.ok("退出登录")));

    }

}

8,SecurityConfig

Spring-Security 配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AdminService adminService;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf
                .disable()
                .sessionManagement()// 基于token,所以不需要session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //退出登陆相关的逻辑
                .logout()
                //自定义退出的url---默认的为/logout
                .logoutUrl("/api/admin/logout")
                //自定义退出成功处理器
                .logoutSuccessHandler(LogoutSuccessHandler())
                .and()
                .authorizeRequests()
                .antMatchers("/api/admin/login")// 对登录注册要允许匿名访问
                .permitAll()
                .antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求
                .permitAll()
                .anyRequest()// 除上面外的所有请求全部需要鉴权认证
                .authenticated();
        // 禁用缓存
        httpSecurity.headers().cacheControl();
        // 添加JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定义未授权和未登录结果返回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public LogoutSuccessHandler LogoutSuccessHandler(){
        return new NRSCLogoutSuccessHandler();
    }

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

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


    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
        return new JwtAuthenticationTokenFilter();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

三,最后

以上是关于 spring-security的内容,在项目中还使用 redis 缓存等。
项目地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
一:RestApi接口增加JWT认证功能<br/> 用户填入用户名密码后,与数据库里存储的用户信息进行比对,如果通过,则认证成功。传统的方法是在认证通过后,创建sesstion,并给客户端返回cookie。 现在我们采用JWT来处理用户名密码的认证。区别在于,认证通过后,服务器生成一个token,将token返回给客户端,客户端以后的所有请求都需要在http头中指定该token。 服务器接收的请求后,会对token的合法性进行验证。验证的内容包括: 内容是一个正确的JWT格式 检查签名 检查claims 检查权限 处理登录 创建一个类JWTLoginFilter,核心功能是在验证用户名密码正确后,生成一个token,并将token返回给客户端: 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法: attemptAuthentication :接收并解析用户凭证。 successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token。 二:授权验证 用户一旦登录成功后,会拿到token,后续的请求都会带着这个token,服务端会验证token的合法性。 创建JwtAuthenticationFilter类,我们在这个类中实现token的校验功能。 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。 如果校验通过,就认为这是一个取得授权的合法请求。 三:SpringSecurity配置 通过SpringSecurity的配置,将上面的方法组合在一起。 这是标准的SpringSecurity配置内容,就不在详细说明。注意其中的 .addFilter(new JWTLoginFilter(authenticationManager())) .addFilter(new JwtAuthenticationFilter(authenticationManager())) 这两行,将我们定义的JWT方法加入SpringSecurity的处理流程中。 四:简单测试 下面对我们的程序进行简单的验证:<br/> 1.请求获取用户列表接口:http://localhost:8080/users/userList接口,会收到403错误<br/> { "timestamp": 1518333248079, "status": 403, "error": "Forbidden", "message": "Access Denied", "path": "http://localhost:8080/users/userList" } curl http://localhost:8080/users/userList<br/> 原因就是因为这个url没有授权,所以返回403<br/> ![输入图片说明](https://gitee.com/uploads/images/2018/0211/154022_8d9806ae_130820.png "jwt-1.png") 2.注册一个新用户<br/> curl -H "Content-Type: application/json" -X POST -d '{<br/> "username": "admin",<br/> "password": "password"<br/> }' http://localhost:8080/users/signup<br/> ![输入图片说明](https://gitee.com/uploads/images/2018/0211/154042_74fb2aa6_130820.png "jwt-2.png") 3.登录,会返回token,在http header中,Authorization: Bearer 后面的部分就是token<br/> curl -i -H "Content-Type: application/json" -X POST -d '{<br/> "username": "admin",<br/> "password": "password"<br/> }' http://localhost:8080/login<br/> 温馨提醒:这里的login方法是spring specurity框架提供的默认登录url ![输入图片说明](https://gitee.com/uploads/images/2018/0211/154308_9576ce90_130820.png "jwt-3.png") 4.用登录成功后拿到的token再次请求/users/userList接口<br/> 4.1将请求中的XXXXXX替换成拿到的token<br/> 4.2这次可以成功调用接口了<br/> curl -H "Content-Type: application/json"<br/> -H "Authorization: Bearer XXXXXX"<br/> "http://localhost:8080/users/userList" ![输入图片说明](https://gitee.com/uploads/images/2018/0211/154315_241cd6b2_130820.png "jwt-4.png")
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值