一、相关技术
1. Maven 项目管理工具
2. MybatisPlus
3. SpringBoot 2.7.0
4. Security 安全框架
5. Jwt
6. easy-captcha 验证码
7. swagger2 3.3.0
swagger2的3.3.0版本相关配置可以看我的相关博客(SpringBoot整合Swagger2)
二、技术简介
1. Security
1.1 简介
Spring Security是Spring家族中的一个重量级安全管理框架,实际上,在Spring Boot出现之前,Spring Security就已经发展了很多年了。Spring Boot为Spring Security提供了自动化配置方案。可以零配置使用Spring Security。
1.2 认证方式
- form表单认证【推荐】
- httpBasic认证
2. Jwt
2.1 简介
Json Web Token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准,特别适用于分布式站点的单点登录(SSO)场景。
JWT广义:JWT就是签发token和校验token的一种机制。
JWT狭义:JWT就是token
基于token的鉴权机制类似于http协议也是无状态的,他不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要考虑用户在哪一台服务器登陆了 这就为应用的扩展提供了便利。
2.2 官网图片描述
官网地址:JSON Web Token Introduction - jwt.io
JSON Web Token Introduction - jwt.io
2.3 Jwt组成
Jwt由三部分组成,头部、有效载荷、签名。
2.3.1 头部
头部用于描述该Jwt的最基本的信息。可以被表示成一个JSON对象。
2.3.2 载荷(Playload)
载荷就是存放有效信息的地方。有效信息包含以下部分:
(1)七个默认字段供选择(供选用)
-
iss (issuer):签发人
-
exp (expiration time):过期时间
-
sub (subject):主题
-
aud (audience):受众
-
nbf (Not Before):生效时间
-
iat (Issued At):签发时间
-
jti (JWT ID):编号
(2)私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64
是对称解密的,意味着该部分信息可以归类为明文信息。
2.3.3 签名
-
Signature 部分是对前两部分的签名,防止数据篡改。
-
需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。
三、代码展示
1. pom.xml
<!-- spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--easy-captcha 验证码-->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
<!-- java-jwt 可以反编码过期token-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
2. SecurityConfig.java(Security安全配置类)
/**
* @author w
* @createDate 2022/6/13
* @description: 安全配置类
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Resource
private MyUserDetailService userDetailService;
@Resource
private MyAuthenticationSuccessHandler successHandler;
@Resource
private MyAuthenticationFailureHandler failureHandler;
/**
* 认证不通过的处理类
*/
@Resource
private MyAuthenticationEntryPointHandler entryPointHandler;
/**
* 令牌认证过滤
*/
@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 匹配器
*/
@Resource
private JwtAuthenticationProvider provider;
@Resource
private MyLogoutSuccessHandler logoutSuccessHandler;
@Resource
private CorsFilter corsFilter;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// jdbc
auth.userDetailsService(userDetailService);
// 自定义匹配器
auth.authenticationProvider(provider);
}
/**
* 自定义登录页面的页面放行
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF禁用,因为不使用session
.csrf().disable()
// 没有token认证不通过
.exceptionHandling().authenticationEntryPoint(entryPointHandler)
.and()
// 基于token不使用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 过滤请求
.authorizeRequests()
// 允许匿名访问,防止重定向锁死
.antMatchers("/captchaImage","/login").anonymous()
.antMatchers("/swagger-ui/**").anonymous()
.antMatchers("/swagger/**").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/v2/**").anonymous()
// 除了以上,均要鉴权
.anyRequest()
.authenticated()
.and() // 登出成功处理
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(corsFilter,JwtAuthenticationFilter.class);
}
}
3. JwtUtil.java(令牌工具类)
/**
* 令牌工具类
*/
public class JwtUtil {
// 服务器的密钥
private static String secret = "123456";
/**
* 生成令牌
*
* @param claims
* @return
*/
public static String genToken(Map<String, String> claims , int expire) {
// 有效时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.MINUTE, expire);
// 载荷
JWTCreator.Builder builder = JWT.create();
claims.forEach((k,v)->builder.withClaim(k,v));
// 设置有效期和签名=>生成令牌
String token = builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(secret));
return token;
}
/**
* 校验令牌
*
* @param token
*/
public static void verifyToken(String token) {
JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
}
/**
* 解析令牌
*
* @param token
* @return
*/
public static String parseToken(String token,String key) {
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
return decodedJWT.getClaim(key).asString();
}
/**
* 反编码过期令牌
* @param token
* @param key
* @return
*/
public static String decodeExpireToken(String token, String key) {
return JWT.decode(token).getClaim(key).asString();
}
}
3. JwtAuthenticationFilter.java(令牌过滤器)
/**
* @author w
* @createDate 2022/6/13
* @description: 令牌过滤器
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
if(requestURI.equals("/captchaImage") || requestURI.equals("/login")){
filterChain.doFilter(request,response);
return;
}
String accessToken = getTokenByRequest(request);
if(StrUtil.isEmpty(accessToken)){
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("没有令牌,请先登录");
out.close();
return;
}
// 把令牌放置到Principal
JwtToken jwtToken = new JwtToken(accessToken);
// 存储上下文认证信息
try {
SecurityContextHolder.getContext().setAuthentication(jwtToken);
} catch (Exception e) {
if(e.getCause()!=null && e.getCause() instanceof TokenExpiredException){
responseFailResult(response,AjaxResult.fail(1001,"访问令牌过期"));
return;
}
// 用户已经退出/用户在其他地方登录
responseFailResult(response,AjaxResult.fail(1002,e.getMessage()));
return;
}
filterChain.doFilter(request,response);
}
@SneakyThrows
private void responseFailResult(HttpServletResponse response, AjaxResult result) {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONUtil.toJsonStr(result));
}
/**
* 从请求头里获取访问令牌
* @param request
* @return
*/
private String getTokenByRequest(HttpServletRequest request) {
String tokenStr = request.getHeader("Authorization");
if(StrUtil.isNotEmpty(tokenStr) && tokenStr.startsWith("Bearer ")){
// 返回令牌
return tokenStr.replace("Bearer ","");
}
return "";
}
}
4. MyAuthenticationEntryPointHandler.java(认证失败处理)
/**
* @author w
* @createDate 2022/6/13
* @description: 认证失败的处理类,返回未授权
*/
@Component
public class MyAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Map<String,Object> map = new HashMap<>();
map.put("status",402);
map.put("msg",e.getMessage());
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
}
5. MyLogoutSuccessHandler.java(登出处理)
/**
* 登出处理
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Map<String,Object> map = new HashMap<>();
map.put("status",200);
map.put("msg","登出成功");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
}
6. JwtAuthenticationProvider.java(自定义匹配器)
/**
* @author w
* @createDate 2022/6/14
* @description: 自定义匹配器
*/
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(JwtToken.class);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String accessToken = (String) authentication.getPrincipal();
//try {
JwtUtil.verifyToken(accessToken);
//} catch (Exception e) {
// throw new AuthenticationCredentialsNotFoundException("令牌校验失败");
//}
String userNo = JwtUtil.parseToken(accessToken, Constants.USERID);
// 获取redis的访问令牌
RedisTemplate redisTemplate = RedisBean.redis;
String refreshToken = (String) redisTemplate.opsForValue().get(Constants.REFRESH_TOKEN_PREFIX + userNo);
if(ObjectUtil.isEmpty(refreshToken)){
throw new AuthenticationCredentialsNotFoundException("用户已退出");
}
// 判断时间戳
String accessTokenCurrentTime = JwtUtil.parseToken(accessToken, Constants.CURRENTTIMEMILLIS);
String refreshTokenCurrentTime = JwtUtil.parseToken(refreshToken, Constants.CURRENTTIMEMILLIS);
if(!accessTokenCurrentTime.equalsIgnoreCase(refreshTokenCurrentTime)){
throw new AuthenticationCredentialsNotFoundException("用户已在别处登录");
}
return new JwtToken(accessToken);
}
}
7. JwtToken.java(认证令牌)
/**
* @author w
* @createDate 2022/6/14
* @description: 认证令牌
*/
public class JwtToken extends AbstractAuthenticationToken {
private String token;
public JwtToken(String token) {
super((Collection)null);
this.setAuthenticated(false);
this.token = token;
}
public JwtToken(Collection<? extends GrantedAuthority> authorities, String token) {
super(authorities);
this.setAuthenticated(true);
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return null;
}
}
8. 登录的controller service serviceImpl
8.1 LoginController.java(登录控制层)
@RestController
public class LoginController {
@Resource
private LoginServiceI loginService;
@PostMapping("/login")
public AjaxResult login(LoginVo loginVo){
AjaxResult ajaxResult = AjaxResult.success();
ajaxResult.put("token",loginService.login(loginVo));
return ajaxResult;
}
}
8.2 LoginServiceI.java(登录业务逻辑接口)
public interface LoginServiceI {
String login(LoginVo loginVo);
}
8.3 LoginServiceImpl.java(登录业务逻辑)
@Service
public class LoginServiceImpl implements LoginServiceI {
@Resource
private SysUserMapper sysUserMapper;
@Resource
private RedisTemplate<String,String> redisTemplate;
/**
* 登录成功返回访问令牌给前端
* @param loginVo
* @return
*/
@Override
public String login(LoginVo loginVo) {
validateCaptcha(loginVo.getCode(), loginVo.getUuid());
// 验证用户名和密码
SysUser sysUserDB = sysUserMapper.selectByUserName(loginVo.getUsername());
if(ObjectUtil.isEmpty(sysUserDB)){
// 用户名不存在
throw new CustomException(CommonCode.USERNAME_ISNOT_EXIST);
}
if("1".equals(sysUserDB.getStatus())){
throw new CustomException(CommonCode.USER_OUTAGE);
}
if("2".equals(sysUserDB.getDelFlag())){
throw new CustomException(CommonCode.USER_IS_DELETE);
}
if(!PwdUtil.encode(loginVo.getPassword(),loginVo.getUsername()).equalsIgnoreCase(sysUserDB.getPassword())){
throw new CustomException(CommonCode.USERNAME_OR_PASSWORD_ERROR);
}
// 生成令牌
// 获得系统毫秒数
long currentTimeMillis = System.currentTimeMillis();
// 载荷信息,JWT令牌中不存放敏感信息
Map<String, String> claims = new HashMap<>();
claims.put(Constants.USERID,String.valueOf(sysUserDB.getUserId()));
claims.put(Constants.USERNAME,sysUserDB.getUserName());
claims.put(Constants.CURRENTTIMEMILLIS,String.valueOf(currentTimeMillis));
// 访问令牌返回给前端
String accessToken = JwtUtil.genToken(claims,Constants.EXPIRE_ACCESS_TOKEN_TIME);
// 刷新令牌写入redis
String refreshToken = JwtUtil.genToken(claims, Constants.EXPIRE_REFRESH_TOKEN_TIME);
redisTemplate.opsForValue().set(Constants.REFRESH_TOKEN_PREFIX+sysUserDB.getUserId(),refreshToken,Constants.EXPIRE_REFRESH_TOKEN_TIME, TimeUnit.MINUTES);
return accessToken;
}
/**
* 验证码认证
* @param code
* @param uuid
*/
private void validateCaptcha(String code, String uuid) {
String key = Constants.CAPTCHA_CODE_PREFIX + uuid;
String realCode = redisTemplate.opsForValue().get(key);
if(StrUtil.isNotEmpty(realCode)){
if(code.equalsIgnoreCase(realCode)){
redisTemplate.delete(key);
return;
}else {
throw new CustomException(CommonCode.CAPTCHA_ERROR);
}
}else {
throw new CustomException(CommonCode.CAPTCHA_EXPIRE);
}
}
9. 授权
在controller层使用注解@PreAuthorize("hasAuthority('xxx:xxx:xxx')")进行授权。
例如:
UserController.java
使用一个方法举例。
@RestController
@RequestMapping("/system/user")
@Api(tags = "用户信息表接口")
public class SysUserController {
/**
* 用户信息表业务层
*/
@Resource
private SysUserServiceI sysUserService;
/**
* 增加用户信息表
* @param sysUser
* @return
*/
@PostMapping
@ApiOperation("增加用户信息表")
@PreAuthorize("hasAuthority('system:user:add')")
public AjaxResult add(@RequestBody SysUser sysUser){
sysUserService.add(sysUser);
return AjaxResult.success();
}
}