用户登录,根据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);
}
}