登录
为了更好的表达整个程序流程,以下顺序全部按照我编码思路流程进行
登录思路
思路:
- 请求登录接口,进入方法
进入一下接口,loginBody中是登录专用实体类,里面只有username和password。
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public Res login(@RequestBody LoginBody loginBody) {
System.out.println("登录"+loginBody);
return sysUserService.login(loginBody);
}
- 用户验证
在登录方法中,首先进行登录验证。至于为什么会进入UserDetailsService,请查看authenticationManager.authenticate()调用UserDetailsServiceImpl.loadUserByUsername过程
//用户验证 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(loginBody.getUserName(), loginBody.getPassword()));
进入到UserDetailsService.loadUserByUsername()中,会返回UserDetails,UserDetails是用户的详细信息。
此时要实现UserDetailsService接口的loadUserByUsername()方法去做一些,原因在下面的代码注释中。
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
/**
* 定义自己的登陆逻辑不走安全框架的默认
* 备注:
* security拥有自己的登录界面和登录逻辑
* 为了使用自己的登录逻辑,创建一个UserDetailServiceImpl实现框架的UserDetailsService
* authenticationManager.authenticate()会调用次方法
* @param username
* @return UserDetails 为了返回用户信息,创建了一个登录类LoginUser继承了UserDetails
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserMapper.selectOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUserName, username));
if (user==null) {
log.info("登录用户:{} 不存在.", username);
throw new RuntimeException("登录用户:" + username + " 不存在");
} else if ("2".equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new RuntimeException("对不起,您的账号:" + username + " 已被删除");
} else if ("2".equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new RuntimeException("对不起,您的账号:" + username + " 已停用");
}
//这是权限,应该从数据库中获取的,为了方便使用,直接在这里写死了,后续使用中因该联合用户信息、角色、菜单查询
Set<String> permissions=new HashSet<>();
permissions.add("admin");
//把获取到的登录用户信息添加到LoginUser返回
LoginUser loginUser=new LoginUser(user.getUserId(),user.getDeptId(),user, permissions);
System.out.println("UserDetailServiceImpl自己定义的登陆逻辑返回结果"+loginUser);
//次方法中返回值是UserDetails,但最后返回的是LoginUser,是因为LoginUser实现了UserDetails类
//注意这里的LoginUser不能是登录的实体类。因为会造成username的null指针异常。还有loginUser中重写的方法给默认值改成true;
return loginUser;
}
}
- 获取用户信息
//获取当前用户信息
LoginUser loginUserInfo = (LoginUser) authentication.getPrincipal();
System.out.println("登陆接口中用户验证结果" + loginUserInfo);
// 生成token
return Res.ok(createToken(loginUserInfo));
从上一步验证中获取到登录用户信息。次方法可以封装成一个SecurityUtils安全服务工具类,用于获取用户的各种信息。
- 生成token信息返回给前端
生成token的流程是,先生成一个随机数,把这个随机数作为key,登录用户信息作为value,存入到redis中,接着把token通过jwt进行加密,把加密过后的随机数返回给前端。在前端进行其他请求的时候,前端在请求头中把加密过后的token传给后台,后台通过jwt解析,把redis中存的key解析出来,再去redis获取用户信息。
public String createToken(LoginUser loginUserInfo) {
//获取一个随机数token
String token = UUID.randomUUID().toString();
loginUserInfo.setToken(token);
//将token作为key,loginUserInfo作为value,存入redis
redisTemplate.opsForValue().set("login_user_key"+token, loginUserInfo);
Map<String, Object> claims = new HashMap<>();
claims.put("uuid", token);
//将生成的随机数进行加密返回给前端,后续过程中,前端会通过传这个个加密之后的token1,然后解密出token随机数,然后去redis中取出用户信息
String token1 = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, "myruoyiitem").compact();
System.out.println("生成token随机数" + token);
System.out.println("对token随机数进行加密" + token1);
return token1;
}
总结
整个流程就是:进入接口—>进行验证—>获取用户信息—>创建token—>存入redis—>返回
注意点
登录所用的实体和实现UserDetails的实体不能是一个,具体的空指针我忘记在哪里出现了,反正遇到这个问题了。
添加认证
认证的目的
目的就是让用户请求的时候,携带加密之后的随机数(token),去验证用户的信息
认证过程
- 创建认证过滤器JwtAuthenticationTokenFilter,记得实现OncePerRequestFilter。下面获取token的过程就是获取请求头token,然后解密出token随机数,然后去redis中取出用户信息
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
TokenUtil tokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("对于在SecurityConfig中没有放行的都进入到这个接口");
//获取token
LoginUser user = tokenUtil.getLoginUser(request);
if (user != null) {
//验证令牌有效期,相差不足20分钟,自动刷新缓存,预防三十分钟之后自动失效
//tokenUtil.verifyToken(user);
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
//放行
filterChain.doFilter(request, response);
}
}
- 创建完成之后,把JwtAuthenticationTokenFilter加入到SecurityConfig过滤器链中
/**
* SecurityConfig设置
* @author lenovo
* @date 2022/6/20
* @EnableGlobalMethodSecurity注解prePostEnabled开启注解功能
*
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* token认证过滤器
*/
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 授权失败处理类
*/
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
// 配置用于放行login页面
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf
http.csrf().disable()
// 认证失败、授权失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedHandler(accessDeniedHandler).and()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login", "/register").anonymous()
.antMatchers("/login", "/register").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//把token校验过滤器添加到过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 将AuthenticationManager作为对象注入容器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 把PasswordEncoder注入bean
* 加密方法
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
备注
1. 为什么设置.antMatchers(“/login”,“/register”).anonymous()?
此时,用户登录的时候就会首先进入JwtAuthenticationTokenFilter,因为没有token,直接放行,但是在SecurityConfig中,并不会放行,请求会返回状态码401,没有认证成功。但是如果设置.antMatchers(“/login”,“/register”).anonymous()之后,请求会放行登录注册接口。
处理认证失败的请求
上面说到,请求认证失败之后会返回给前端一个状态码401,此时,我们需要做一下异常处理。来处理一下这个401状态,使返回的状态使是200,code=401
/**
* 认证失败处理类
*
* @author ruoyi
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException {
int code = 401;
String msg = "请求访问:{" + request.getRequestURI() + "},认证失败,无法访问系统资源";
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSON.toJSONString(Res.failed(code,msg)));
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
处理之后并不是就这样能生效,而是还需要再SecurityConfig配置这个异常处理
http.csrf().disable()
// 认证失败、授权失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
这样才能生效。
添加授权
一般我们都是使用注解的方式去进行授权。
在请求带 @PreAuthorize(“@ss.hasPermi(‘admin’)”)的接口中。都进行授权。也需要一个授权的服务。注意注解ss是服务中名称。
/**
* 测试token
*
* @return 结果
*/
@GetMapping("/testToken")
@PreAuthorize("@ss.hasPermi('admin')")
public Res testToken() {
System.out.println("测试token");
return Res.ok("测试通过");
}
自定义权限实现
/**
* 自定义权限实现,ss取自SpringSecurity首字母
*
* @author lenovo
* @date 2022/6/21
*/
@Service("ss")
public class PermissionService {
/**
* 所有权限标识
*/
private static final String ALL_PERMISSION = "*:*:*";
/**
* 管理员角色权限标识
*/
private static final String SUPER_ADMIN = "admin";
private static final String ROLE_DELIMETER = ",";
private static final String PERMISSION_DELIMETER = ",";
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermi(String permission) {
System.out.println("拥用这个权限才能通过"+permission);
if (StringUtils.isEmpty(permission)) {
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
System.out.println("当前用户信息"+loginUser);
if (loginUser==null|| CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
return hasPermissions(loginUser.getPermissions(), permission);
}
/**
* 判断是否包含权限
*
* @param permissions 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
private boolean hasPermissions(Set<String> permissions, String permission)
{
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
}
重点:配置完成之后,也别注意,此时注解还不能使用,需要在SecurityConfig开启注解。再SecurityConfig类加上注解开启
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
处理授权失败异常
/**
* 授权失败处理类
* @author lenovo
* @date 2022/6/21
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
int code = 403;
String msg = "请求访问:{" + request.getRequestURI() + "},失败,权限不足";
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSON.toJSONString(Res.failed(code,msg)));
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
这样,如果授权失败,请求返回的是一个403状态,根据认证失败的处理过程,同理授权页需要再SecurityConfig配置授权
http.csrf().disable()
// 认证失败、授权失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedHandler(accessDeniedHandler).and()
注册注意的点
在使用注册的时候,加密方式一定要和登录相同,
数据库添加用户信息时,密码一定要生成BCryptPasswordEncoder密码再添加。