spring security+jwt实现认证授权

执行流程

用户登录,根据url跳转到spring security内置好的UserDetailService中进行认证,最后会把返回的authentication放入到securityContext中,然后会调用认证成功的AuthenticationSuccessHandler,这里面会从authentication中获取用户对象,然后把这个对象转换成一个vo对象(这里随自己的喜好而定,主要目的是返回一个jwt,此jwt无论是后端还是前端封装,最后都会把他放入到以后的请求头或者请求体中),并返回给前端,然后用户每次携带这个jwt进行访问时,首先会经过自定义的一个过滤器,他的目的是从request中解析出jwt(无论有没有jwt,都会放行进入下一个filter),然后通过这个jwt解析出一个唯一的标识(我这里使用的是uuid,以这个uuid作为主键,存放此用户的权限和过期时间等信息),然后通过uuid查询数据库,redis或者session获取这个用户的信息和权限,并把它封装到UsernamePasswordAuthenticationToken中,然后把这个对象放入securityContext中,调用其他过滤器,最后会走到controller中,判断接口是否有@PreAuthorize注解,若有则会取出authentication中的权限进行比较(这都是spring security自己内部做的),若判断此用户有此权限则会调用方法,若没有则会返回没有权限

代码

首先自定义securityConfig继承WebSecurityConfigureAdapter,注意的是上面要加注解@EnableGlobalMethodSecurity(prePostEnabled = true)

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

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private UserDetailServiceImpl userDetailService;

    @Autowired
    private TokenFilter tokenFilter;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //自定义userDetailsService,并放入到security中
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(bCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**",
                        "/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**",
                        "/statics/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginProcessingUrl("/login")
                    .successHandler(authenticationSuccessHandler)
                    .failureHandler(authenticationFailureHandler)
                    .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(authenticationEntryPoint)
                .and()
                .logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)
                .and()
                .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

然后把所有的登录成功,登录失败,游客,退出登录成功的处理器整合在一个类中

@Configuration
public class SecurityHandlerConfig {

    @Autowired
    private TokenService tokenService;

    //登录认证成功返回token
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                //这里之前已经调用过loaduserbyusername方法,security把loginuser封装成authentication,这里从authentication中获取loginuser
                LoginUser loginUser = (LoginUser) authentication.getPrincipal();
                //然后通过这个loginuser的信息,进行一些列的操作创建一个存有jwt的vo对象
                Token token = tokenService.saveToken(loginUser);
                ResponseUtil.responseJson(httpServletResponse, HttpStatus.OK.value(), token);
            }
        };
    }

    //登录失败
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                String msg = e.getMessage();
                ResponseUtil.responseJson(httpServletResponse, HttpStatus.UNAUTHORIZED.value(), new ResponseInfo(HttpStatus.UNAUTHORIZED.value()+"",msg));
            }
        };
    }

    //未登录,用来解决匿名用户访问无权限资源时的异常
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                ResponseUtil.responseJson(httpServletResponse, HttpStatus.UNAUTHORIZED.value(), new ResponseInfo(HttpStatus.UNAUTHORIZED.value()+"", "请先登录"));
            }
        };
    }

    //退出处理
    @Bean
    public LogoutSuccessHandler logoutSuccessHandler() {
        return new LogoutSuccessHandler() {
            @Override
            public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                ResponseInfo info = new ResponseInfo(HttpStatus.OK.value() + "", "退出成功");
                String token = TokenUtils.getToken(httpServletRequest);
                tokenService.deleteToken(token);
                ResponseUtil.responseJson(httpServletResponse, HttpStatus.OK.value(), info);
            }
        };
    }

}

之后是security经典的UserDetailService
首先定义User类,对应数据库的User表,然后定义一个LoginUser实现UserDetail对象,尤其实现getAuthorities方法

@Data
public class SysUser extends BaseEntity<Long> {

	private static final long serialVersionUID = -6525908145032868837L;

	private String username;
	private String password;
	private String nickname;
	private String headImgUrl;
	private String phone;
	private String telephone;
	private String email;
	@JsonFormat(pattern = "yyyy-MM-dd")
	private Date birthday;
	private Integer sex;
	private Integer status;
	private String intro;
}
public class LoginUser extends SysUser implements UserDetails {

	private static final long serialVersionUID = -1379274258881257107L;

	private List<Permission> permissions;
	private String token;
	/** 登陆时间戳(毫秒) */
	private Long loginTime;
	/** 过期时间戳 */
	private Long expireTime;

	public List<Permission> getPermissions() {
		return permissions;
	}

	public void setPermissions(List<Permission> permissions) {
		this.permissions = permissions;
	}

	public String getToken() {
		return token;
	}

	public void setToken(String token) {
		this.token = token;
	}

	@Override
	@JsonIgnore
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return permissions.parallelStream().filter(p -> !StringUtils.isEmpty(p.getPermission()))
				.map(p -> new SimpleGrantedAuthority(p.getPermission())).collect(Collectors.toSet());
	}

	public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
		// do nothing
	}

	// 账户是否未过期
	@JsonIgnore
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	// 账户是否未锁定
	@JsonIgnore
	@Override
	public boolean isAccountNonLocked() {
		return getStatus() != Status.LOCKED;
	}

	// 密码是否未过期
	@JsonIgnore
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	// 账户是否激活
	@JsonIgnore
	@Override
	public boolean isEnabled() {
		return true;
	}

	public Long getLoginTime() {
		return loginTime;
	}

	public void setLoginTime(Long loginTime) {
		this.loginTime = loginTime;
	}

	public Long getExpireTime() {
		return expireTime;
	}

	public void setExpireTime(Long expireTime) {
		this.expireTime = expireTime;
	}

}

然后是UserDetailServiceImpl实现UserDetailService实现loadUserByUsername方法

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Autowired
    private PermissionDao permissionDao;

    //逻辑为获取用户表sysuser和permission,并把他们封装成loginuser,之后会调用认证成功的方法
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userService.getUser(username);
        if (ObjectUtils.isEmpty(sysUser)) {
            throw new AuthenticationCredentialsNotFoundException("用户名不存在");
        } else if (sysUser.getStatus() == SysUser.Status.LOCKED) {
            throw new LockedException("用户被锁定,请联系管理员");
        } else if (sysUser.getStatus() == SysUser.Status.DISABLED) {
            throw new DisabledException("用户已作废");
        }
        LoginUser loginUser = new LoginUser();
        BeanUtils.copyProperties(sysUser, loginUser);
        List<Permission> permissions = permissionDao.listByUserId(sysUser.getId());
        loginUser.setPermissions(permissions);
        return loginUser;
    }
}

sql就不贴了,烂大街
此时security已经把获取的loginUser封装成authentication,并把他放入到securityContext中了,已经执行到登录成功的处理器方法中,他会通过获取的loginUser保存到数据库,并生成jwt

@Service
public class TokenServiceImpl implements TokenService {

    /**
     * token过期秒数
     */
    @Value("${token.expire.seconds}")
    private Integer expireSeconds;

    /**
     * 私钥
     */
    @Value("${token.jwtSecret}")
    private String jwtSecret;

    private static Key KEY = null;
    private static final String LOGIN_USER_KEY = "LOGIN_USER_KEY";

    @Autowired
    private TokenDao tokenDao;

    // 首先是通过随机生成的uuid,存储到loginUser中的token属性中,然后把uuid存放到token的id字段中,最后把uuid封装成jwt返回给前端,
    // 每次请求前端会携带这个jwt经过tokenFilter过滤,而后端获取到jwt之后,对jwt进行解码获取uuid,然后查询数据库获取这个用户的val也就是此用户的LoginUser
    @Override
    public Token saveToken(LoginUser loginUser) {
        //设置loginUser的属性,,保存一个通过uuid为主键的对象,并通过uuid生成jwt
        loginUser.setToken(UUID.randomUUID().toString());
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireSeconds * 1000);
        //把用户的token和用户的信息存放到数据库中
        TokenDB tokenDB = new TokenDB();
        tokenDB.setId(loginUser.getToken());
        tokenDB.setCreateTime(new Date());
        tokenDB.setUpdateTime(new Date());
        tokenDB.setExpireTime(new Date());
        tokenDB.setVal(JSONObject.toJSONString(loginUser));
        tokenDao.save(tokenDB);
        String jwtToken = createJWTToken(loginUser);
        //最后返回一个带有jwt的vo
        return new Token(jwtToken, loginUser.getLoginTime());
    }

    // 用于用户退出
    @Override
    public boolean deleteToken(String token) {
        String uuid = this.getUUIDFromJWT(token);
        if (!StringUtils.isEmpty(uuid)) {
            TokenDB tokenDB = tokenDao.getById(uuid);
            LoginUser loginUser = this.toLoginUser(tokenDB);
            if (!ObjectUtils.isEmpty(loginUser)) {
                tokenDao.delete(uuid);
                return true;
            }
        }
        return false;
    }

    // 当用户的token快过期的时候,会通过此方法刷新过期时间
    @Override
    public void refresh(LoginUser loginUser) {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireSeconds * 1000);
        TokenDB tokenDB = tokenDao.getById(loginUser.getToken());
        tokenDB.setExpireTime(new Date());
        tokenDB.setUpdateTime(new Date());
        tokenDB.setVal(JSONObject.toJSONString(loginUser));
        tokenDao.update(tokenDB);
    }

    // 从tokenDB中获取loginUser
    @Override
    public LoginUser getLoginUser(String token) {
        String uuid = getUUIDFromJWT(token);
        if (!StringUtils.isEmpty(uuid)) {
            TokenDB tokenDB = tokenDao.getById(uuid);
            return this.toLoginUser(tokenDB);
        }
        return null;
    }

    //通过从数据库中的token表中获取的用户的val并转换成loginUser
    private LoginUser toLoginUser(TokenDB tokenDB) {
        if ((!ObjectUtils.isEmpty(tokenDB)) && tokenDB.getExpireTime().getTime() > System.currentTimeMillis()) {
            return JSONObject.parseObject(tokenDB.getVal(), LoginUser.class);
        }
        return null;
    }

    /**
     * 生成jwt
     * 也就是把uuid生成的token进行加密
     *
     * @param loginUser
     * @return
     */
    private String createJWTToken(LoginUser loginUser) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(LOGIN_USER_KEY, loginUser.getToken());// 放入一个随机字符串,通过该串可找到登陆用户
        String jwtToken = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, getKeyInstance())
                .compact();
        return jwtToken;
    }

    private Key getKeyInstance() {
        if (KEY == null) {
            synchronized (TokenServiceImpl.class) {
                if (KEY == null) {// 双重锁
                    byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(jwtSecret);
                    KEY = new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
                }
            }
        }
        return KEY;
    }

    private String getUUIDFromJWT(String jwt) {
        if ("null".equals(jwt) || StringUtils.isEmpty(jwt)) {
            return null;
        }
        Map<String, Object> jwtClaims = null;
        try {
            jwtClaims = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwt).getBody();
            return MapUtils.getString(jwtClaims, LOGIN_USER_KEY);
        } catch (ExpiredJwtException e) {
            System.out.println(jwt+"已过期");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return null;
    }

}

public class Token implements Serializable {

	private static final long serialVersionUID = 6314027741784310221L;

	private String token;
	/** 登陆时间戳(毫秒) */
	private Long loginTime;

	public Token(String token, Long loginTime) {
		super();
		this.token = token;
		this.loginTime = loginTime;
	}

	public String getToken() {
		return token;
	}

	public void setToken(String token) {
		this.token = token;
	}

	public Long getLoginTime() {
		return loginTime;
	}

	public void setLoginTime(Long loginTime) {
		this.loginTime = loginTime;
	}

}

@Data
public class TokenDB extends BaseEntity<String> {

    private static final long serialVersionUID = 4566334160572911795L;

    /**
     * 过期时间
     */
    private Date expireTime;
    /**
     * LoginUser的json串
     */
    private String val;

}

工具类啥的不贴了

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/hello")
    @PreAuthorize("hasAuthority('sys:user:hello')")
    public String hello() {
        return "hello security";
    }

    @GetMapping("/current")
    public SysUser currentUser() {
        return UserUtil.getLoginUser();
    }

    @GetMapping("/{id}")
    @PreAuthorize("hasAuthority('sys:user:query')")
    public SysUser user(@PathVariable Long id) {
        return userService.getById(id);
    }
}

你可能感兴趣的:(SpringSecurity)