一、添加依赖
com.baomidou
mybatis-plus-boot-starter
3.5.1
org.projectlombok
lombok true
mysql
mysql-connector-java
8.0.32
com.nimbusds
nimbus-jose-jwt
9.11.1
com.alibaba
fastjson
2.0.25
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
二、创建四层架构
1.SecurityApplication(主方法)
package com.security;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.security.mapper")//扫描mapper层
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
2.JWTUtil(工具类)
package com.security.util;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.util.Map;
/**
* jwt 工具类
* 1: 创建jwt
* 2:解密jwt
*/
@Component
public class JWTUtil {
private static final String KEY="ksdjfvhwhfowsrhfgdfhgdrygheqweqweqeqweqwreafwseefgwetgffgwsdfgwgweftgsdgdrhg";
public static String createJWT(Map map) throws JOSEException {
//第一部分 头部,主要防jwt自我描述部分,比如加密方式
JWSHeader header=new JWSHeader.Builder(JWSAlgorithm.HS256)
.type(JOSEObjectType.JWT).build();
//第二部分 载荷部分,主要放用户登录成功后,一些个人信息(注意:不要防敏感信息)
Payload payload=new Payload(map);
//第三部分 签名部分,(头部+载荷)通过密钥加密后得到的
JWSObject jwsObject=new JWSObject(header,payload);
JWSSigner jwsSigner=new MACSigner(KEY);
//拿到密钥加密
jwsObject.sign(jwsSigner);
return jwsObject.serialize();
}
/**
* 拿到jwt 根据系统密钥,看能不能解开
* @return
*/
public static boolean decode(String jwt) throws ParseException, JOSEException {
//parse() 把字符串转成一个对象
JWSObject jwsObject=JWSObject.parse(jwt);
JWSVerifier jwsVerifier=new MACVerifier(KEY);
//解密方法 verify()
return jwsObject.verify(jwsVerifier);
}
/**
* 根据jwt 获取其中载荷部分
* @param jwt
* @return
*/
public static Map getPayLoad(String jwt) throws ParseException {
//parse() 把字符串转成一个对象,并解密
JWSObject jwsObject=JWSObject.parse(jwt);
Payload payload=jwsObject.getPayload();
Map map=payload.toJSONObject();
return map;
}
}
二、创建实体类与枚举
1.Users
用户信息实体类
package com.security.entity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.util.List;
@Data
public class Users {
@JsonSerialize(using = ToStringSerializer.class)//防止json传过来导致精度缺失
private Long id;
private String userName;
private String account;
private String password;
private List anths;//用户权限
}
2.ResponseEnum
枚举HttpResult层中所用静态方法
package com.security.entity;
public enum ResponseEnum {
LOGIN_SUCCESS(200,"OK"),
LOGIN_FAIL(500,"NO"),
NO_LOGIN(501,"NO_LOGIN"),
NO_AUTH(502,"NO_AUTH")
;
private Integer code;
private String msg;
ResponseEnum() {
}
ResponseEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
3.HttpResult
HttpResult是后端向前端传输数据时所用实体类
package com.security.entity;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class HttpResult {
private Integer code;
private String msg;
private T data;
public HttpResult ok(T t){
return new HttpResult(ResponseEnum.LOGIN_SUCCESS.getCode(), t);
}
public static final HttpResult LOGIN_SUCCESS=
new HttpResult(ResponseEnum.LOGIN_SUCCESS.getCode(),
ResponseEnum.LOGIN_SUCCESS.getMsg());
public static final HttpResult LOGIN_FAIL=
new HttpResult(ResponseEnum.LOGIN_FAIL.getCode(),
ResponseEnum.LOGIN_FAIL.getMsg());
public static final HttpResult NO_LOGIN=
new HttpResult(ResponseEnum.NO_LOGIN.getCode(),
ResponseEnum.NO_LOGIN.getMsg());
public static final HttpResult NO_AUTH=
new HttpResult(ResponseEnum.NO_AUTH.getCode(),
ResponseEnum.NO_AUTH.getMsg());
public HttpResult(Integer code, T data) {
this.code = code;
this.data = data;
}
public HttpResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
三、配置.yml文件
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/2_27user?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
logging:
level:
com.woniu.dao: debug
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n'
四、配置.xml文件(维护SQL语句)
创建getUserInfoByAccount方法,通过account获取用户信息
ps:数据库:
t_user(用户信息表)
t_anth (角色表)
user_anth(用户角色关系表)
五、mapper与service层
1.UserMapper
package com.security.mapper;
import com.security.entity.Users;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper {
/**
* 根据账号查看用户信息及其权限
* @param account
* @return
*/
Users getUserInfoByAccount(String account);
}
2.SecurityService
package com.security.service;
import com.security.entity.Users;
import com.security.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class SecurityService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserMapper userMapper;
/**
* username:页面传过来的username
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users=userMapper.getUserInfoByAccount(username);
if(users!=null) {
String anths=String.join(",",users.getAnths());
//username 数据库产查用户信息
return new User(users.getAccount(), passwordEncoder.encode(users.getPassword()),
AuthorityUtils.commaSeparatedStringToAuthorityList(anths));
}
else {
throw new UsernameNotFoundException("该用户不存在");
}
}
}
六、创建handler(处理器)与config(配置)层
七、配置handler(处理器)层
1.LoginFailHandler(登录失败处理器)
package com.security.handler;
import com.alibaba.fastjson.JSON;
import com.security.entity.HttpResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class LoginFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e)
throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(HttpResult.LOGIN_FAIL));
}
}
2.LoginSuccessHandler(登录成功处理器)
package com.security.handler;
import com.alibaba.fastjson.JSON;
import com.security.entity.HttpResult;
import com.security.util.JWTUtil;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 登录成功处理器
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private StringRedisTemplate redisTemplate;
@SneakyThrows//注解抛异常
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication)
throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
User user=(User)authentication.getPrincipal();
String username=user.getUsername();
Map map=new HashMap();
map.put("username",username);
String jwt=JWTUtil.createJWT(map);
//拿jwt干啥:1、放到redis,2、把jwt传到前端
redisTemplate.opsForValue().set("jwt:"+username,jwt,30, TimeUnit.MINUTES);
httpServletResponse.getWriter().write(JSON.toJSONString(new HttpResult().ok(jwt)));
}
}
3.NoLoginHandler(未登录处理器)
package com.security.handler;
import com.alibaba.fastjson.JSON;
import com.security.entity.HttpResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 用户未登录:访问系统接口
*/
public class NoLoginHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e)
throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(HttpResult.NO_LOGIN));
}
}
4.NoAuthHandler(无权限处理器)
package com.security.handler;
import com.alibaba.fastjson.JSON;
import com.security.entity.HttpResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class NoAuthHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AccessDeniedException e)
throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(HttpResult.NO_AUTH));
}
}
5.JWTFilter(jwt过滤器)
package com.security.handler;
import com.security.config.SecurityConfig;
import com.security.service.SecurityService;
import com.security.util.JWTUtil;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 检验jwt
* 1、判断请求头是否携带jwt
* 否:放行不处理
* 是:走到第二步
* 2、对前端传过来的jwt解密
* 否:放行不处理
* 是:走到第三步
* 3、获取Redis的jwt
* 获取不到:放行不处理
* 获取到:走到第四步
* 4、对比jwt
* 否:放行不处理
* 是:走到第五步
* 5、给jwt续期
*/
@Component
public class JWTFilter extends OncePerRequestFilter {
@Autowired
private SecurityService securityService;
/**
* StringRedisTemplate和RedisTemplate
*/
@Autowired
private StringRedisTemplate redisTemplate;
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain)
throws ServletException, IOException {
/**
* * 1、判断请求头是否携带jwt
* * 否:放行不处理
* * 是:走到第二步
*/
String jwt=httpServletRequest.getHeader("jwt");
if(jwt==null){
filterChain.doFilter(httpServletRequest,httpServletResponse);//交给过滤器处理
return;
}
/**
* * 2、对前端传过来的jwt解密
* * 否:放行不处理
* * 是:走到第三步
*/
if(!JWTUtil.decode(jwt)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
/**
* * 3、获取Redis的jwt
* * 获取不到:放行不处理
* * 获取到:走到第四步
*/
Map payLoad=JWTUtil.getPayLoad(jwt);
String username=(String) payLoad.get("username");
String redisJwt=redisTemplate.opsForValue().get("jwt:"+payLoad.get("username"));
if(redisJwt==null){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
/**
* * 4、对比jwt
* * 否:放行不处理
* * 是:走到第五步
*/
if(!jwt.equals(redisJwt)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
/**
* * 5、给jwt续期
*/
redisTemplate.opsForValue().set("jwt:"+payLoad.get("username"),jwt,30, TimeUnit.MINUTES);
//把用户信息放到security容器中
UserDetails userDetails=securityService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken upa=new UsernamePasswordAuthenticationToken( userDetails.getUsername(),
userDetails.getPassword(),
userDetails.getAuthorities());
//把信息放到security容器中
SecurityContextHolder.getContext().setAuthentication(upa);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
八、配置congfig层
1.CorsConfig(处理跨域申请)
package com.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class CorsConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry){
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(false)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
2.SecurityConfig(Spring Security配置文件)
package com.security.config;
import com.security.handler.*;
import com.security.service.SecurityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,jsr250Enabled = true,prePostEnabled = true) //作用:自动开启注解式授权
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JWTFilter jWTFilter;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private SecurityService securityService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(securityService);
}
/**
* 自定义登录页面
* @param http
* @throws Exception
*/
public void configure(HttpSecurity http) throws Exception {
http.formLogin() //告诉框架自定义页面
.loginPage("/login.html") //登录页面地址
.loginProcessingUrl("/dologin") //对应表单提交的action
.successHandler(loginSuccessHandler)
.failureHandler(new LoginFailHandler())
.permitAll();//对login.html和dologin请求放行
http.exceptionHandling()
.accessDeniedHandler(new NoAuthHandler())
.authenticationEntryPoint(new NoLoginHandler());
http.authorizeRequests()
// .antMatchers("/hello").hasAuthority("anth_techer") //有anth_techer的权限才能访问/hello
// .antMatchers("/hello").hasAnyAuthority("anth_techer","hello") //有anth_techer或hello的权限才能访问/hello
// .antMatchers("/hello").hasRole("anth_techer") //会在anth_techer加ROLE_前缀
// .antMatchers("/hello").permitAll() //配置免登陆接口
.anyRequest().authenticated(); //所有请求都拦截
/**
* 把jwtfilter注入进来
*/
http.addFilterAfter(jWTFilter, UsernamePasswordAuthenticationFilter.class);
/**
* 把session禁掉
*/
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//防跨站脚本攻击关闭
http.csrf().disable();
//运行跨域
http.cors();
}
/**
* 数据加密类
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
ps:配置登录页面
登录
九、配置控制层
1.UserController
package com.security.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
// @Secured("ROLE_anth_techer") //访问页面所需权限
// @PermitAll//全部可访问
// @PreAuthorize("hasAuthority('anth_techer')")
@RequestMapping("/hello")
public String hello(){
return "hello";
}
}