1、用户实体类
该类实现了UserDetails接口
public class SysUser implements UserDetails {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "用户编号")
@TableId(value = "user_id", type = IdType.ID_WORKER_STR)
private String userId;
@ApiModelProperty(value = "用户名")
private String userName;
@ApiModelProperty(value = "用户手机号码 ")
private String userPhone;
@ApiModelProperty(value = "用户密码")
private String userPassword;
@ApiModelProperty(value = "用户最近一次登录时间")
private String userLastLoginTime;
@ApiModelProperty(value = "用户注册时间")
private String userCreateTime;
@ApiModelProperty(value = "用户状态,0正常,-1删除")
private Integer userStatus;
@ApiModelProperty(value = "用户的角色列表")
private List roleCodes;
//重写UserDetails中的方法,得到权限列表,权限列表中存储的是角色名
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Collection authorities = new ArrayList<>();
//将用户的角色以SimpleGrantedAuthority的形式存入authorities中
for(String roleCode : roleCodes) {
if(StringUtils.isEmpty(roleCode)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(roleCode);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return userPassword;
}
@Override
public String getUsername() {
return userName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2、登录过滤器
内含登录成功或失败后的处理方法
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
/**
* 该redisTemplate不能直接由容器注入
* 因为TokenLoginFilter类不在spring容器内,所以redisTemplate不能直接注入
* 该redisTemplate是通过MyWebSecurityConfig中TokenLoginFilter的构造条件注入的。
* 也就是下面 public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate)这个方法
*/
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
//从MyWebSecurityConfig得到redisTemplate
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
//自定义登录url
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login","POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) {
try {
//从请求中读取数据
LoginForm user = new ObjectMapper().readValue(req.getInputStream(), LoginForm.class);
//两种抛异常方法
//自定义AccountException
if (user.getUserphone() == null || user.getUserphone().equals("")){
throw new AccountException("账号为空");
}
//security中AuthenticationException的自雷异常
if (user.getPassword() == null || user.getPassword().equals("")){
throw new BadCredentialsException("密码为空");
}
//调方法
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUserphone(), user.getPassword());
return authenticationManager.authenticate(token);
} catch (IOException e) {
ResponseUtil.out(res, Result.code(ResultCode.fail).message("数据读取错误"));
}
return null;
}
/**
* 登录成功
* 成功后创建token返回前端,并将用户权限存入redis
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
SysUser user = (SysUser) auth.getPrincipal();
String userPhone = user.getUserPhone();
String userName = user.getUsername();
String jwtToken = JwtUtils.getJwtToken(userPhone, userName);
redisTemplate.opsForValue().set(userPhone,user.getRoleCodes());
redisTemplate.opsForValue().set("token",jwtToken);
ResponseUtil.out(response, Result.code(ResultCode.SUCCESS).message("登录成功!").data("token", jwtToken));
}
/**
* 登录失败
* 失败后提示用户登录失败
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(response, Result.code(ResultCode.fail).message(e.getMessage()));
}
}
2.1、LoginForm类
用一个简单的类来接受前端出来的登录信息
public class LoginForm {
private String userphone;
private String password;
}
3、MyUserDetailsService类
实现UserDetailsService接口,重写loadUserByUsername方法,按自己的实际需求来编写验证规则
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService userService;
/***
* 根据账号获取用户信息
*/
@Override
public UserDetails loadUserByUsername(String userphone) throws UsernameNotFoundException {
//调用userService得到SysUser
SysUser user = userService.findByPhone(userphone);
if (user == null){
throw new AccountException("账号或密码错误");
}
//SysUser继承了UserDetails接口,可直接返回
return user;
}
}
4、TokenAuthenticationFilter类
该类为token校验器,并封装了用户权限,保存至security上下文中
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
private RedisTemplate redisTemplate;
//之所以redisTemplate能生效,是因为该RedisTemplate是在MyWebSecurityConfig传入的
public TokenAuthenticationFilter(AuthenticationManager authManager, RedisTemplate redisTemplate) {
super(authManager);
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String token = request.getHeader("token");
String jwtToken = (String) redisTemplate.opsForValue().get("token");
if (token == null || !token.equals(jwtToken) ){
//token为空时,直接放行到下一条过滤器(此时SecurityContext中没有任何权限,放行后会被最终的过滤器检测到无权限,然后禁止访问)
chain.doFilter(request, response);
System.out.println("当token为空或格式错误时 直接放行");
return;
}
//根据token获得authenticationToken
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(token);
//将authenticationToken存入SecurityContext
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
chain.doFilter(request, response);
}
/**
* 这里从token中获取用户信息并新建一个UsernamePasswordAuthenticationToken
*/
private UsernamePasswordAuthenticationToken getAuthentication(String token) {
Claims claims = JwtUtils.getClaims(token);
String userPhone = (String) claims.get("userPhone");
String userName = (String) claims.get("userName");
if (userPhone != null && userName != null) {
/**
* 1、从redis中取出用户拥有的角色
* 2、将其转化为SimpleGrantedAuthority
* 3、封装至UsernamePasswordAuthenticationToken,方便后面鉴权时取出
* UsernamePasswordAuthenticationToken是接口Authentication的一个实现类
*/
List roleCodes = (List)redisTemplate.opsForValue().get(userPhone);
Collection authorities = new ArrayList<>();
for(String roleCode : roleCodes) {
if(StringUtils.isEmpty(roleCode)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(roleCode);
authorities.add(authority);
}
//todo
System.out.println(authorities+"==============");
return new UsernamePasswordAuthenticationToken(userPhone, userName, authorities);
}
return null;
}
}
5、统一异常处理
//该类处理认证异常
public class MyAuthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, Result.code(ResultCode.LOGIN_ERR).message("请登录后再进行操作!"));
}
}
//该类处理鉴权异常
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
ResponseUtil.out(httpServletResponse, Result.code(ResultCode.ACCESS_NOT).message("您的权限不足!"));
}
}
6、注销处理
public class MyLogoutHandler implements LogoutHandler {
private RedisTemplate redisTemplate;
public MyLogoutHandler(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader("token");
String userPhone = JwtUtils.getUserPhoneByJwtToken(request);
if (token != null){
//清除缓存里的信息
redisTemplate.delete(userPhone);
redisTemplate.delete("token");
}
ResponseUtil.out(response, Result.code(ResultCode.SUCCESS).message("退出成功"));
}
}
6、security配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解模式!!
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
//未授权
@Autowired
private MyAuthorizedEntryPoint myAuthorizedEntryPoint;
//访问拒绝
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
//在WebSecurityConfig中注入,为了后面传入其他的组件
@Autowired
private RedisTemplate redisTemplate;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 设置默认的加密方式(强hash方式加密)
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置认证方式等,数据库中存储的是加密后的密码
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//http相关的配置,包括登入登出、异常处理、会话管理等
http.cors().and().csrf().disable();//关闭了csrf拦截的过滤器
http.authorizeRequests().
// antMatchers("/getUser").hasAuthority("query_user").
//所有请求都需要被认证
anyRequest().authenticated().
and().formLogin().usernameParameter("userphone").permitAll().//允许所有用户
and().logout().logoutUrl("/user/logout").addLogoutHandler(new MyLogoutHandler(redisTemplate)).
//未认证和未授权时的处理
and().exceptionHandling().authenticationEntryPoint(myAuthorizedEntryPoint).accessDeniedHandler(myAccessDeniedHandler).
and()
//关闭session 用token验证,所以关闭session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
and()
//不能用自动装配方式,因为authenticationManager不能自动装配
//登录过滤器,同时成功后创建token,该过滤器因为没有注入到spring容器中,所以创建一个构造方法,在配置中将redisTemplate传入该过滤器中
.addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate))
//Token,同时成功后创建token,该过滤器因为没有注入到spring容器中,所以创建一个构造方法,在配置中将redisTemplate传入该过滤器中
.addFilter(new TokenAuthenticationFilter(authenticationManager(), redisTemplate)).httpBasic();
}
}
7、postman测试
首先SysUserController中有三个测试接口,第一个接口认证后即可访问,第二个接口需要登录的用户拥有ROLE_ADMIN角色,第三个接口需要用户拥有ROLE_USER角色。
@GetMapping("/lande")
public String lande(){
return "hello,lande";
}
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
@GetMapping("/test")
public String test1(){
return "ceshihahaha";
}
@PreAuthorize("hasAnyRole('ROLE_USER')")
@GetMapping("/hello")
public String hello(){
return "hellohahaha";
}
登录:该用户仅拥有ROLE_USER角色
返回了token信息
测试第一个接口:
请求头中带上token,因为security配置类中关闭了session,后续请求必须带上token才能访问。
访问成功。
测试第二个接口
该接口需要ROLE_ADMIN,我们已登录的用户只拥有ROLE_USER,所以该接口不能访问。
结果符合预期
测试第三个接口
该接口需要ROLE_USER,已登录用户可以访问
结果符合预期
如果请求头中不带token或token错误
项目源码地址:https://github.com/lan-de/SpringSecurity-01