ok,上面我们说的流程中涉及到几个组件,有些是我们需要根据实际情况来重写的。因为我们是使用json数据进行前后端数据交互,并且我们返回结果也是特定封装的。我们先再总结一下我们需要了解的几个组件:
首先我们来解决用户认证问题,分为首次登陆,和二次认证。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>com.github.axetgroupId>
<artifactId>kaptchaartifactId>
<version>0.0.9version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.3.3version>
dependency>
这里使用到了Redis工具类
Redis工具类参考博客
记得配置Redis序列化规则
Redis序列化配置参考博客
数据库访问使用到了MybatisPuls
自行参考其他MP相关文档
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
//边框
properties.put("kaptcha.border", "no");
//文本颜色
properties.put("kaptcha.textproducer.font.color", "black");
//字符串空行
properties.put("kaptcha.textproducer.char.space", "4");
//高度
properties.put("kaptcha.image.height", "40");
//宽
properties.put("kaptcha.image.width", "120");
//字符大小
properties.put("kaptcha.textproducer.font.size", "30");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
@Api(tags = "图片验证码相关")
@RestController
public class AuthController extends BaseController{
@Autowired
private Producer producer;
@ApiOperation("生成图片验证码")
@GetMapping("/captcha")
public Result captcha() throws IOException {
String key = "aaaaa";//UUID.randomUUID().toString();
//生成验证码 5位数随机验证码
String code = "11111";//producer.createText();
//生成图片
BufferedImage image=producer.createImage(code);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(image,"jpg",out);
//转换成base64位编码
BASE64Encoder encoder = new BASE64Encoder();
String str="data:image/jpeg;base64,";//前缀
String base64Img = str + encoder.encode(out.toByteArray());
redisUtil.hset(Const.CAPTCHA_KEY,key,code,120);
return Result.success(
MapUtil.builder().put("token", key)
.put("captchaImg", base64Img)
.build()
);
}
}
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
/**
* JWT
* @author Tu_Yooo
* @Date 2021/5/25 15:54
*/
@Data
@Component
@ConfigurationProperties(prefix = "tutony.jwt")
public class JwtUtils {
//过期时间
private Long expire;
//密钥
private String secret;
//JWT名称
private String header;
//生成Jwt
public String generateToken(String username){
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire );
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate)// 7天過期
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
//解析Jwt
public Claims getClaimsByToken(String jwt){
try{
return Jwts.parser()
.setSigningKey(secret) //密钥
.parseClaimsJws(jwt)
.getBody();
}catch (Exception e){
return null;
}
}
//判断Jwt是否过期
public boolean isTokenExpired(Claims claims){
return claims.getExpiration().before(new Date());
}
}
# JWT
tutony:
jwt:
header: Authorization
expire: 604800 # 7天秒单位
secret: f4e2e52034348f86b67cde581c0f9eb5
由于我们是前后端分离项目,无论是否登录成功返回给前端都应该是一个JSON格式的数据
/**
* 统一的结果集返回
* @author Tu_Yooo
* @Date 2021/5/24 16:50
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result implements Serializable {
//200是正常,非200表示异常
private Integer code;
// 结果消息
private String msg;
//结果数据
private Object data;
/调用正常时返回///
public static Result success(Integer code,String msg,Object data){
return new Result(code,msg,data);
}
public static Result success(Integer code,Object data){
Result result = new Result();
result.setCode(code);
result.setData(data);
return result;
}
public static Result success(Object data){
Result result = new Result();
result.setCode(200);
result.setData(data);
return result;
}
/异常时返回//
public static Result fail(Integer code,String msg,Object data){
return new Result(code,msg,data);
}
public static Result fail(Integer code,String msg){
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
}
public static Result fail(String msg){
Result result = new Result();
result.setCode(400);
result.setMsg(msg);
result.setData(null);
return result;
}
}
/**
* Security 登录处理
* AuthenticationFailureHandler 失败处理
* AuthenticationSuccessHandler 成功处理
* @author Tu_Yooo
* @Date 2021/5/25 14:23
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler, AuthenticationSuccessHandler {
//登录失败时处理
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream out = httpServletResponse.getOutputStream();
Result fail = Result.fail(e.getMessage());
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
@Autowired
private JwtUtils jwtUtils;
//登录成功时处理
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream out = httpServletResponse.getOutputStream();
//生成JWT,并放置到请求头中
String jwt = jwtUtils.generateToken(authentication.getName());
httpServletResponse.setHeader(jwtUtils.getHeader(),jwt);
Result fail = Result.success(jwt);
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
}
此过滤器放置在Security用户密码认证过滤器之前,先一步校验验证码,如果通过,继续验证用户名密码,如果不通过则抛出异常
/**
* 验证码认证过滤器
* 图片验证码校验过滤器,在登录过滤器前
* OncePerRequestFilter
* @author Tu_Yooo
* @Date 2021/5/25 14:55
*/
@Component
public class CaptchaFilter extends OncePerRequestFilter {
//自定义Security登录成功或失败处理
@Autowired
private LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String url = httpServletRequest.getRequestURI();
//只需要拦截 /login POST请求
if ("/login".equals(url)&& httpServletRequest.getMethod().equals("POST")){
try{
//校验验证码
vaildata(httpServletRequest);
}catch (CaptchaException e){
//如果不正确,就调整到认证失败处理器
loginFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
}
}
//验证成功 则继续往下面走
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
@Autowired
private RedisUtil redisUtil;
//校验验证码
private void vaildata(HttpServletRequest request){
//用户输入的验证码
String code = request.getParameter("code");
//验证码 在Redis中的Key
String token = request.getParameter("token");
if (StringUtils.isBlank(token) || StringUtils.isBlank(code)){
throw new CaptchaException("验证码错误");
}
if (!redisUtil.hexists(Const.CAPTCHA_KEY, token)){
throw new CaptchaException("验证码失效");
}
if(!code.equals(redisUtil.hget(Const.CAPTCHA_KEY,token))){
throw new CaptchaException("验证码错误");
}
//清除redis 验证码一次性使用
redisUtil.hdel(Const.CAPTCHA_KEY,token);
}
}
清空JWT
/**
* 登出操作 清空数据
* @author Tu_Yooo
* @Date 2021/5/27 10:15
*/
@Component
public class JWTLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//如果权限信息不为空 需要进行一个手动的登出操作
if (authentication != null){
new SecurityContextLogoutHandler().logout(httpServletRequest,httpServletResponse,authentication);
}
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream out = httpServletResponse.getOutputStream();
//生成JWT,并放置到请求头中
httpServletResponse.setHeader(jwtUtils.getHeader(),"");
Result fail = Result.success("登出成功");
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
}
用户访问其他接口时,我们会判断是否携带JWT,从JWT中取出用户信息,查询数据库此用户关联的权限信息,一并封装到Security中
/**
* 自定义一个过滤器用来进行识别jwt。
* 实现自动登录
* @author Tu_Yooo
* @Date 2021/5/25 17:14
*/
@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private SysUserService sysUserService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(jwtUtils.getHeader());
if(StrUtil.isBlankOrUndefined(header)){
chain.doFilter(request,response);
return;
}
//工具类解析JWT
Claims token = jwtUtils.getClaimsByToken(header);
if (token == null){
throw new JwtException("token异常");
}
if (jwtUtils.isTokenExpired(token)){
log.info("jwt过期");
throw new JwtException("token过期");
}
String username = token.getSubject();
//查询数据库-用户名关联的用户
SysUser user = sysUserService.getByUsername(username);
//获取用户权限等信息
UsernamePasswordAuthenticationToken authentication =
// 参数: 用户名 密码 权限信息
new UsernamePasswordAuthenticationToken(username,null,userDetailsService.getUserAuthority(user.getId()));
//后续security就能获取到当前登录的用户信息了,也就完成了用户认证。
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request,response);
}
}
这里使用UserDetailsServiceImpl
查询了用户id关联的权限信息,我们写在后面
当JWT过期或者出现异常时会交给此过滤器进行统一处理
/**
* JWT识别异常处理
* @author Tu_Yooo
* @Date 2021/5/26 9:53
*/
@Slf4j
@Component
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.error("认证失败!未登录!");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);//401
ServletOutputStream out = httpServletResponse.getOutputStream();
Result fail = Result.fail("请先登录!");
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
}
/**
* 其他异常处理
* @author Tu_Yooo
* @Date 2021/5/26 10:01
*/
@Slf4j
@Component
public class JWTAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
log.info("权限不够!!");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);//403
ServletOutputStream out = httpServletResponse.getOutputStream();
Result fail = Result.fail(e.getMessage());
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
}
security从数据库中获取用户信息
需要重写 UserDetailsService
/**
* security从数据库中获取用户信息
*
* 需要重写 UserDetailsService
* @author Tu_Yooo
* @Date 2021/5/26 10:31
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//数据库中获取用户信息
SysUser sysUser = sysUserService.getByUsername(username);
if (sysUser == null)
throw new UsernameNotFoundException("用户名或密码不正确");
//参数: 用户id 用户名 用户密码 权限信息
return new AccountUser(sysUser.getId(),sysUser.getUsername(),sysUser.getPassword(),getUserAuthority(sysUser.getId()));
}
/**
* 获取用户权限信息 <角色 菜单>
* @param userId 用户id
* @return 权限信息
*/
public List<GrantedAuthority> getUserAuthority(Long userId){
//角色(ROLE_admin) 菜单操作权限 sys:user:list,sys:user:save
String authority = sysUserService.getUserAuthority(userId);
//将字符串通过工具类进行解析
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
}
返回结果UserDetails
默认实现类是User
,为了方便后续,新增其他字段我们重写它,使用自己的UserDetails
/**
*
* 封装用户信息
*
* UserDetails 默认实现类 User
* 我们需要去重写它 方便日后字段扩展
* @author Tu_Yooo
* @Date 2021/5/26 10:45
*/
public class AccountUser implements UserDetails {
///扩展字段 用户id
private Long userId;
private static final long serialVersionUID = 530L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;//密码
private final String username; //用户名
private final Collection<? extends GrantedAuthority> authorities; //权限信息
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public AccountUser(Long userId,String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId,username, password, true, true, true, true, authorities);
}
public AccountUser(Long userId,String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (username != null && !"".equals(username) && password != null) {
this.userId=userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
这里我们使用了sysUserService
查询了数据库权限信息
它必须是一段逗号分割的字符串并交给Security的工具类进行解析
如:ROLE_admin,sys:user:list,sys:user:save
数据库大致如下,建表语句我们提供在后面:
查询用户id关联的权限信息和角色,查询完成之后解析成字符串并返回
/**
* 根据用户id 获取用户关联的权限信息
* @param userId 用户id
* @return 权限信息字符串
*/
@Override
public String getUserAuthority(Long userId) {
String authority = "";
//角色 ROLE_admin
List<SysRole> roles = sysRoleService.list(new QueryWrapper<SysRole>()
.inSql("id", "select role_id from sys_user_role where user_id =" + userId));
System.out.println(roles);
if (roles.size() > 0){
authority=roles.stream().map(r -> "ROLE_"+r.getCode()).collect(Collectors.joining(",")).concat(",");
}
//获取菜单操作权限编码
List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);
if (menuIds.size() > 0){
List<SysMenu> menus = sysMenuService.listByIds(menuIds);
String menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
authority = authority.concat(menuPerms);
}
return authority;
}
所有的准备工作完成以后,我们需要编写Security配置类,集成所有的过滤器组件
/**
* Security
* @author Tu_Yooo
* @Date 2021/5/25 12:49
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解标注 哪些方法需要鉴权 @PreAuthorize("hasRole('admin')") @PreAuthorize("hasAuthority('sys:user:save')")
public class Securityconfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private CaptchaFilter captchaFilter;
//JWT异常处理
@Autowired
private JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// 其他异常处理
@Autowired
private JWTAccessDeniedHandler jwtAccessDeniedHandler;
//自定义JWT识别
@Bean
JWTAuthenticationFilter jwtAuthenticationFilter() throws Exception {
return new JWTAuthenticationFilter(authenticationManager());
}
//数据库加密方式
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
//数据库获取用户信息
@Autowired
private UserDetailsServiceImpl userDetailsService;
//登出
@Autowired
private JWTLogoutSuccessHandler jwtLogoutSuccessHandler;
//不需要拦截的白名单
private static final String[] URL_WHITELIST={
"/login",
"/logout",
"/captcha",
"/favicon.ico",
"/swagger-ui.html"
};
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
//允许跨域 关闭csrf
http.cors().and().csrf().disable()
//登录配置
.formLogin()
.successHandler(loginFailureHandler) //自定义登录成功时处理
.failureHandler(loginFailureHandler) //自定义登录失败时处理
// 登出配置
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler) //登出成功时处理
//禁用Session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不生成session
//配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll() //白名单中路径 不需要拦截
.anyRequest().authenticated()
//异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint) //JWT异常处理
.accessDeniedHandler(jwtAccessDeniedHandler)//其他异常处理
//配置自定义过滤器
.and()
//自定义JWT识别过滤器
.addFilter(jwtAuthenticationFilter())
//自定义验证码过滤器 在 账户密码过滤器之前执行
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//数据库认证方式
auth.userDetailsService(userDetailsService);
}
}
/**
* MVC扩展配置
* @author Tu_Yooo
* @Date 2021/5/25 12:35
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 解决跨域问题
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
当我们集成了Security后,就可以使用它的内置注解来标注controller接口,完成权限控制
Security内置的权限注解:
比如需要Admin角色权限:
@PreAuthorize("hasRole('admin')")
比如需要添加管理员的操作权限
@PreAuthorize("hasAuthority('sys:user:save')")
ok,我们再来整体梳理一下授权、验证权限的流程:
@Autowired
private SysUserService sysUserService;
@PreAuthorize("hasRole('admin')")
@GetMapping("/test")
public Result test(){
return Result.success(sysUserService.list());
}
本文参考B站UP主MarkerHub视频笔记整理视频链接
参考文档:MP使用及建表语句都在其中哟~