原始认证流程通常会配合Session一起使用,但前后端分离后就用不到Session了
SpringSecurity默认的认证流程如下图(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)
2、前后端分离认证流程DaoAuthenticationProvider继承AbstractUserDetailsAuthenticationProvider抽象类,而AbstractUserDetailsAuthenticationProvider抽象类又实现了AuthenticationProvider这个接口。
AuthenticationProvider接口和AuthenticationManager接口都有 authenticate() 这个方法
认证流程:
1、传入用户名和密码
2、UsernamePasswordAuthenticationFilter会把用户名和密码封装成Authentication对象
3、然后又再调用AuthenticationManager接口中的authenticate()方法进行认证,在AuthenticationManager接口的实现类ProviderManager中又调用了重写的authenticate()方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
4、AbstractUserDetailsAuthenticationProvider的authenticate()方法中调用了抽象方法retrieveUser()方法
5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法
6、loadUserByUsername()方法会返回UserDetails对象,认证成功逐一返回上一层
前后端分离后,我们要求在认证成功或者失败的时候能够返回对应的状态码,这时我们不再使用Session进行认证管理,而常采用jwt(JSON Web Token)的方式进行认证,这里引出两种前后端分离的写法
(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)
2.1、继承UsernamePasswordAuthenticationFilter的写法无论使用下面哪一种写法,这里都需要在UsernamePasswordAuthenticationFilter前面添加一个过滤器,用于进行Token认证,如果Token认证成功,则表示该用户已登录;Token认证失败则表明未登录或者登陆已过期。
2.2、自定义写法认证流程:
1、传入用户名和密码
2、MyUsernamePasswordAuthenticationFilter会把用户名和密码封装成Authentication对象
3、然后又再调用AuthenticationManager接口中的authenticate()方法进行认证,在AuthenticationManager接口的实现类ProviderManager中又调用了重写的authenticate()方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
4、AbstractUserDetailsAuthenticationProvider的authenticate()方法中调用了抽象方法retrieveUser()方法
5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法
6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象
7、在继承WebSecurityConfigurerAdapter的类中设置登陆成功、失败处理器,处理器内部定义好返回的状态码等信息
2.3、区别UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken抽象类,AbstractAuthenticationToken抽象类实现了Authentication接口
认证流程:
1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层
2、Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象
3、然后调用AuthenticationManager的authenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
4、AbstractUserDetailsAuthenticationProvider的authenticate()方法中调用了抽象方法retrieveUser()方法
5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法
6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象
二、数据库的设计1、使用UsernamePasswordAuthenticationFilter的写法需要使用登陆成功、失败处理器,自定义的写法不需要,自定义的写法可以自定义失败处理器(包括认证异常和授权异常,即登陆失败和没有权限)
2、使用UsernamePasswordAuthenticationFilter的写法对于扩展写法没那么友好,比如说添加手机验证码
该示例是上面自定义的前后端分离的写法
1、用户表 2、用户角色关系表 3、角色表 4、图片表 5、点赞表 三、初始配置这里使用的是Oracle数据库,这里没有权限的表,但是使用角色来判断也差不多
1、项目结构 2、导入依赖SpringBoot 版本是 2.6.0
3、代码生成器org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-security org.springframework.security spring-security-test test com.baomidou mybatis-plus-boot-starter 3.4.1 org.springframework.boot spring-boot-starter-data-redis com.alibaba fastjson 1.2.74 cn.hutool hutool-all 5.5.6 com.baomidou mybatis-plus-generator 3.4.1 org.apache.commons commons-lang3 3.7 org.apache.velocity velocity-engine-core 2.2 io.springfox springfox-swagger-ui 2.7.0 io.springfox springfox-swagger2 2.7.0 io.jsonwebtoken jjwt 0.9.0 mysql mysql-connector-java runtime com.oracle.database.jdbc ojdbc8 runtime
代码生成器这里最开始使用的是mysql 8.X版本的,读者需要自己修改一下数据库的名字,如果是mysql 5.X还需要修改一下驱动
后面才改用Oracle数据库,这里的代码就懒得改了
package com.guet.APPshareimage;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.apache.commons.lang3.StringUtils;
import java.util.Scanner;
public class CodeGenerator {
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 创建代码生成器对象
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir(scanner("请输入你的项目路径") + "/src/main/java");
//作者
gc.setAuthor("LZDWTL");
//生成之后是否打开资源管理器
gc.setOpen(false);
//重新生成时是否覆盖文件
gc.setFileOverride(false);
//%s 为占位符
//mp生成service层代码,默认接口名称第一个字母是有I
gc.setServiceName("%sService");
//设置主键生成策略 自动增长
gc.setIdType(IdType.AUTO);
//设置Date的类型 只使用 java.util.date 代替
gc.setDateType(DateType.ONLY_DATE);
//开启实体属性 Swagger2 注解
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/shareimage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("shareimage");
dsc.setPassword("888888");
//使用mysql数据库
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
//pc.setModuleName(scanner("请输入模块名"));
pc.setParent("com.guet.APPshareimage");
pc.setController("controller");
pc.setService("service");
pc.setServiceImpl("service.impl");
pc.setMapper("mapper");
pc.setEntity("entity");
pc.setXml("mapper");
mpg.setPackageInfo(pc);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
//设置哪些表需要自动生成
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
//实体类名称驼峰命名
strategy.setNaming(NamingStrategy.underline_to_camel);
//列名名称驼峰命名
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//使用简化getter和setter
strategy.setEntityLombokModel(true);
//设置controller的api风格 使用RestController
strategy.setRestControllerStyle(true);
//驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
//忽略表中生成实体类的前缀
//strategy.setTablePrefix("t_");
mpg.setStrategy(strategy);
mpg.execute();
}
}
运行代码生成器,复制路径输入,然后依次输入数据库中表的名字
D:WorkSpaceJavaWorkSpceidealAPP-shareimageAPP-shareimage t_user,t_picture,t_like,t_user_role,t_role4、配置application.yml
根据自己的数据库和redis进行配置
server:
port: 8080
spring:
# 数据库配置
datasource:
driver-class-name: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@120.77.80.135:1521:orcl
username: XXXXXX
password: XXXXXX
# 连接池
hikari:
# 连接池名
pool-name: DateHikariCP
# 最小空闲连接数
minimum-idle: 5
# 空闲连接最大存活时间,默认600000(10分钟)
idle-timeout: 180000
# 最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接自动提交
auto-commit: true
# 连接最大存活时间,1800000(30分钟)
max-lifetime: 1800000
# 连接超时时间,默认30000(30秒)
connection-timeout: 30000
# 测试连接是否可用的查询语句
#connection-test-query: SELECt 1 #这个是mysql的测试语句
connection-test-query: SELECT * from dual #这个是oracle的测试语句
#redis配置
redis:
#服务器地址
host: 120.77.80.135
#端口
port: 6379
#redis密码
password: XXXXXX
#数据库,默认是0
database: 0
#超时时间
timeout: 1209600000ms
lettuce:
pool:
#最大链接数,默认8
max-active: 8
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接,默认8
max-idle: 200
#最小空闲连接,默认0
min-idle: 5
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.guet.APPshareimage.entity
logging:
level:
com.guet.shareimage.mapper: debug
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 加解密使用的密钥
secret: lzdwtl
# JWT的超期限时间(1000*60*60*24*14)14天,即两周
expiration: 1209600000
# JWT 负载中拿到开头
tokenHead: Bearer
role:
roleid: 1
5、其他配置、工具类
5.1、SpringSecurity配置类
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyOncePerRequestFilter myOncePerRequestFilter;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/doLogin").anonymous()
//其余url都要认证才能访问
.anyRequest().authenticated();
}
}
5.2、JSON格式返回配置类
public abstract class JSONAuthentication {
protected void WriteJSON(HttpServletRequest request,
HttpServletResponse response,
Object obj) throws IOException, ServletException {
//这里很重要,否则页面获取不到正常的JSON数据集
response.setContentType("application/json;charset=UTF-8");
//跨域设置
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Method", "POST,GET");
//输出JSON
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(obj));
out.flush();
out.close();
}
}
5.3、密码编码类
@Component
public class BCryptPasswordEncoderUtil extends BCryptPasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return super.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return super.matches(rawPassword,encodedPassword);
}
}
5.4、JWT工具类
@Component
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
private static String SECRET_KEY;
private static Long EXPIRATION_TIME;
//对于静态变量,需要使用set方法才能使用设置好的字段值
@Value("${jwt.secret}")
public void setSECRET_KEY(String SECRET_KEY) {
this.SECRET_KEY = SECRET_KEY;
}
@Value("${jwt.expiration}")
public void setEXPIRATION_TIME(Long expiration) {
this.EXPIRATION_TIME = expiration;
}
public static String getUUID() {
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = EXPIRATION_TIME;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("LZDWTL") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static SecretKey generalKey() {
byte[] encodedKey = base64.getDecoder().decode(SECRET_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
5.5、Redis工具类
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
public void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
public T getCacheObject(final String key)
{
ValueOperations operation = redisTemplate.opsForValue();
return operation.get(key);
}
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
public long setCacheList(final String key, final List dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
public List getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
public BoundSetOperations setCacheSet(final String key, final Set dataSet)
{
BoundSetOperations setOperation = redisTemplate.boundSetOps(key);
Iterator it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
public Set getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
public void setCacheMap(final String key, final Map dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
public Map getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
public void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
public T getCacheMapValue(final String key, final String hKey)
{
HashOperations opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
public void delCacheMapValue(final String key, final String hkey)
{
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}
public List getMultiCacheMapValue(final String key, final Collection
5.6、Redis配置类
package com.guet.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate
5.7、序列化工具
public class FastJsonRedisSerializer四、全局异常处理 1、公用返回对象 1.1、拓展接口implements RedisSerializer { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private Class clazz; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } public FastJsonRedisSerializer(Class clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return JSON.parseObject(str, clazz); } protected JavaType getJavaType(Class> clazz) { return TypeFactory.defaultInstance().constructType(clazz); } }
使公用返回对象枚举类和自定义异常方便扩展
public interface CommonResp {
Integer getCode();
String getMsg();
CommonResp setMsg(String msg);
}
1.2、公用返回对象
@Data
public class RespBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private String msg;
private Object obj;
public RespBean(RespBeanEnum respBeanEnum, Object obj) {
this.code = respBeanEnum.getCode();
this.msg = respBeanEnum.getMsg();
this.obj = obj;
}
public RespBean(RespBeanEnum respBeanEnum) {
this.code = respBeanEnum.getCode();
this.msg = respBeanEnum.getMsg();
}
public RespBean(RespBeanEnum respBeanEnum, String msg) {
this.code = respBeanEnum.getCode();
this.msg = msg;
}
public RespBean() {
this.code = RespBeanEnum.ERROR.getCode();
this.msg = RespBeanEnum.ERROR.getMsg();
}
public RespBean(String msg) {
this.code = RespBeanEnum.ERROR.getCode();
this.msg = msg;
}
//自定义的业务异常错误码和信息
public RespBean(ServicesException e) {
this.code = e.getCode();
this.msg = e.getMsg();
}
}
1.3、枚举类
public enum RespBeanEnum implements CommonResp{
SUCCESS(200,"请求成功!"),
ERROR(500,"服务器响应错误!"),
USER_REGISTER_FAILED(1001, "注册失败"),
USER_ACCOUNT_EXISTED(1002,"用户名已存在"),
USER_ACCOUNT_NOT_EXIST(1003,"用户名不存在"),
USERNAME_PASSWORD_ERROR(1004,"用户名或密码错误"),
PASSWORD_ERROR(1005,"密码错误"),
USER_ACCOUNT_EXPIRED(1006,"账号过期"),
USER_PASSWORD_EXPIRED(1007,"密码过期"),
USER_ACCOUNT_DISABLE(1008,"账号不可用"),
USER_ACCOUNT_LOCKED(1009,"账号锁定"),
USER_NOT_LOGIN(1010,"用户未登陆"),
USER_NO_PERMISSIONS(1011,"用户权限不足"),
USER_SESSION_INVALID(1012,"会话已超时"),
USER_ACCOUNT_LOGIN_IN_OTHER_PLACE(1013,"账号超时或账号在另一个地方登陆"),
TOKEN_VALIDATE_FAILED(1014,"Token令牌验证失败"),
LIKE_ALREADY_GICED(1015,"请勿重复点赞"),
PICTURE_UPLOAD_FAILED(2001,"上传图片失败"),
GIVE_LIKE_FAILED(2002,"点赞失败"),
PICTURE_LOAD_FAILED(2003,"图片加载失败"),
UPDATE_USER_INFO_FAILED(2004,"修改用户信息失败"),
UPDATE_USER_PASSWORD_FAILED(2005,"修改密码失败"),
;
private Integer code;
private String msg;
RespBeanEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getMsg() {
return this.msg;
}
@Override
public CommonResp setMsg(String msg) {
this.msg=msg;
return this;
}
}
2、全局异常
2.1、自定义异常
实现CommonResp接口,方便自定义异常后续修改错误信息
public class ServicesException extends RuntimeException implements CommonResp {
private CommonResp commonResp;
//直接接收RespBeanEnum的传参用于构造业务异常
public ServicesException(CommonResp commonResp) {
super(); //调用父类的无参构造方法
this.commonResp = commonResp;
}
//接收自定义msg的方式构造业务异常
public ServicesException(String msg, CommonResp commonResp) {
super();
this.commonResp = commonResp;
this.commonResp.setMsg(msg);
}
@Override
public Integer getCode() {
return this.commonResp.getCode();
}
@Override
public String getMsg() {
return this.commonResp.getMsg();
}
@Override
public CommonResp setMsg(String msg) {
this.commonResp.setMsg(msg);
return this;
}
}
2.2、全局异常处理器
@RestControllerAdvice注解表示捕获控制层抛出的异常
@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常
(图片来源:https://blog.csdn.net/weixin_43702146/article/details/118606502)
因为使用了@RestControllerAdvice注解,自动去捕获控制层抛出的异常,AuthenticationException异常和AccessDeniedException异常也被捕获了,但是我不想在这里处理,所以将这两个异常往外抛给失败处理器去处理。
@RestControllerAdvice //捕获controller层的异常
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(value = ServicesException.class)
public RespBean servicesExceptionHandler(ServicesException e){
logger.error("发生业务异常! 原因是:{}",e.getMsg());
return new RespBean(e);
}
@ExceptionHandler(value = Exception.class)
public RespBean exceptionHandler(Exception e){
logger.error("未知异常! 原因是:",e);
return new RespBean();
}
@ExceptionHandler(value = AuthenticationException.class)
public void accountExpiredExceptionHandler(AuthenticationException authException){
throw authException;
}
//将 AccessDeniedException 异常往上抛,让授权处理器去处理
@ExceptionHandler(value = AccessDeniedException.class)
public void accessDeniedExceptionHandler(AccessDeniedException accDenException){
throw accDenException;
}
}
五、登陆认证
1、登陆模块UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken抽象类,AbstractAuthenticationToken抽象类实现了Authentication接口
认证流程:
1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层
2、Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象
3、然后调用AuthenticationManager的authenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
4、AbstractUserDetailsAuthenticationProvider的authenticate()方法中调用了抽象方法retrieveUser()方法
5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法
6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象
1.1、控制器LoginController包括登陆和登出功能
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/doLogin")
public RespBean doLogin(@RequestBody LoginDTO loginDTO){
return loginService.doLogin(loginDTO);
}
@RequestMapping("/doLogout")
public RespBean doLogout(){
return loginService.doLogout();
}
}
1.2、业务层
1.2.1、LoginServiceService层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象
public interface LoginService {
RespBean doLogin(LoginDTO loginDTO);
RespBean doLogout();
}
1.2.2、LoginServiceImpl
这里的 AuthenticationManager 需要在 SpringSecurity 中使用 authenticationManagerBean() 方法才能调用AuthenticationManager的authenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
这里把生成的Token和查询到的用户信息存到Redis中,方便后续使用
@Service
public class LoginServiceImpl implements LoginService {
private static final Logger logger = LoggerFactory.getLogger(LoginServiceImpl.class);
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private TUserService userService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoderUtil passwordEncoder;
@Autowired
private RedisCache redisCache;
@Override
public RespBean doLogin(LoginDTO loginDTO) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
//用户名密码错误
throw new ServicesException(RespBeanEnum.USERNAME_PASSWORD_ERROR);
}
AuthUser authUser = (AuthUser) authenticate.getPrincipal();
String username = authUser.getTUser().getUsername();
String token = JwtUtil.createJWT(username);
//把token和用户信息存到redis中
redisCache.setCacheObject("Token_" + username, token);
redisCache.setCacheObject("UserDetails_" + username, authUser);
//将用户存入上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
Map map = new HashMap<>();
map.put("token", token);
return new RespBean(RespBeanEnum.SUCCESS, map);
}
@Override
public RespBean doLogout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
AuthUser authUser = (AuthUser) authentication.getPrincipal();
String username = authUser.getTUser().getUsername();
//删除redis中存的信息
redisCache.deleteObject("Token_" + username);
redisCache.deleteObject("UserDetails_" + username);
//清除上下文
SecurityContextHolder.clearContext();
return new RespBean(RespBeanEnum.SUCCESS);
}
}
1.2.3、TUserService
public interface TUserService extends IService1.2.4、TUserServiceImpl{ TUser getUserByUserName(String username); }
TUserMapper需要继承baseMapper才能使用selectOne()这个方法
@Service public class TUserServiceImpl extends ServiceImpl1.3、实现 UserDetails 接口implements TUserService { @Value("${role.roleid}") private Integer roleId; @Autowired private TUserMapper userMapper; @Override public TUser getUserByUserName(String username) { LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); //查询条件:全匹配账号名,和状态为1的账号 lambdaQueryWrapper .eq(TUser::getUsername, username); //用getOne查询一个对象出来 // TUser user = this.getOne(lambdaQueryWrapper); TUser user = userMapper.selectOne(lambdaQueryWrapper); //这个与上面的getOne有无区别? return user; } }
@Data
@AllArgsConstructor //全参构造
@NoArgsConstructor //无参构造
public class AuthUser implements UserDetails {
private TUser tUser;
// @JSonField(serialize = false)
private Collection extends GrantedAuthority> authorities;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return tUser.getPassword();
}
@Override
public String getUsername() {
return tUser.getUsername();
}
// 账户是否未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账户是否未被锁
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
1.4、实现UserDetailsService 接口
重写UserDetailsService接口的 loadUserByUsername()方法,在loadUserByUsername()方法中,会查询用户和权限(这里没有权限表,所以查的是角色),然后返回UserDetails对象
@Service(value = "userDetailsService")
public class AuthUserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(AuthUserDetailsServiceImpl.class);
@Autowired
private TUserService userService;
@Autowired
private TRoleService roleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TUser user = userService.getUserByUserName(username);
if (user == null) {
//用户名不存在
throw new ServicesException(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);
} else {
//查找角色,实际应该查询权限,但我数据库没有设计所以就查角色就好了
List roles = roleService.getRolesByUserName(username);
List authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
System.out.println("AuthUserDetailsServiceImpl-loadUserByUsername......user ===> " + user);
return new AuthUser(user, authorities);
}
}
}
1.5、Mapper
1.5.1、TUserMapper
@Mapper public interface TUserMapper extends baseMapper2、Token 认证模块 2.1、认证过滤器{ }
@Component
public class MyOncePerRequestFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(MyOncePerRequestFilter.class);
@Value("${jwt.tokenHeader}")
private String header;
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// header的值是在yml文件中定义的 “Authorization”
String token = request.getHeader(header);
System.out.println("MyOncePerRequestFilter-token = " + token);
if (!StrUtil.isEmpty(token)) {
String username = null;
try {
Claims claims = JwtUtil.parseJWT(token);
username = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
// throw new ServicesException("非法Token,请重新登陆", RespBeanEnum.ERROR);
WriteJSON(request,response,new RespBean(RespBeanEnum.ERROR,"非法Token,请重新登陆"));
return;
}
String redisToken = redisCache.getCacheObject("Token_" + username);
System.out.println("MyOncePerRequestFilter-redisToken = " + redisToken);
if (StrUtil.isEmpty(redisToken)) {
//token令牌验证失败
// throw new ServicesException(RespBeanEnum.TOKEN_VALIDATE_FAILED);
//输出JSON
WriteJSON(request,response,new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED));
return;
}
//对比前端发送请求携带的的token是否与redis中存储的一致
if (!Objects.isNull(redisToken) && redisToken.equals(token)) {
AuthUser authUser = redisCache.getCacheObject("UserDetails_" + username);
System.out.println("MyOncePerRequestFilter-authUser = " + authUser);
if (Objects.isNull(authUser)) {
// throw new ServicesException(RespBeanEnum.USER_NOT_LOGIN);
WriteJSON(request,response,new RespBean(RespBeanEnum.USER_NOT_LOGIN));
return;
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
chain.doFilter(request, response);
}
private void WriteJSON(HttpServletRequest request,
HttpServletResponse response,
Object obj) throws IOException, ServletException {
//这里很重要,否则页面获取不到正常的JSON数据集
response.setContentType("application/json;charset=UTF-8");
//跨域设置
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Method", "POST,GET");
//输出JSON
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(obj));
out.flush();
out.close();
}
}
2.2、SpringSecuity配置类
在配置类中使用addFilterBefore()方法让认证过滤器MyOncePerRequestFilter添加在UsernamePasswordAuthenticationFilter这个过滤器前面
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyOncePerRequestFilter myOncePerRequestFilter;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/doLogin").anonymous()
//其余url都要认证才能访问
.anyRequest().authenticated();
//3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
六、授权
使用 @PreAuthorize注解需要在SpringSecurity配置类中使用下面的语句才能开启方法级的安全
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RestController
@RequestMapping("/user")
public class TUserController {
@RequestMapping("/hello")
//对于hasRole这个方法来讲,ROLE_ 加不加都可以,它的方法会自动判断的
@PreAuthorize("hasRole('ROLE_user')")
public String test() {
return "Hello Login Success!";
}
}
这样就可以了,因为前面已经写好了一些关联的代码,所以在访问该URL的时候,会执行hasRole()这个方法,然后查询AuthUser类(AuthUser类就是实现了UserDetails接口的实现类)中的属性authorities,只要authorities中包含"ROLE_user",则该用户就可以访问这个URL,否则会报错,提示权限不足。
七、自定义失败处理器 1、认证失败处理器注意访问一些需要认证后才能访问的URL时,记得带上token和content-type。
我这里的token的key是Authorization,这个是在application.yml文件中定义的,可以自行修改
继承自定义的JSON格式输出类JSONAuthentication输出JSON格式,同时在里面判断是什么异常做针对性输出
@Component
public class MyAuthenticationEntryPoint extends JSONAuthentication implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(MyAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//用户未登录或者身份校验失败
// RespBean respBean = new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED);
// this.WriteJSON(request, response, respBean);
RespBean respBean;
if (authException instanceof AccountExpiredException) {
//账号过期
respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_EXPIRED);
} else if (authException instanceof InternalAuthenticationServiceException) {
//用户不存在
respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);
} else if (authException instanceof BadCredentialsException) {
//用户名或密码错误(也就是用户名匹配不上密码)
respBean = new RespBean(RespBeanEnum.USERNAME_PASSWORD_ERROR);
} else if (authException instanceof CredentialsExpiredException) {
//密码过期
respBean = new RespBean(RespBeanEnum.USER_PASSWORD_EXPIRED);
} else if (authException instanceof DisabledException) {
//账号不可用
respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_DISABLE);
} else if (authException instanceof LockedException) {
//账号锁定
respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_LOCKED);
} else {
//其他错误
respBean = new RespBean(RespBeanEnum.USER_NOT_LOGIN);
}
//打印错误
logger.error(String.valueOf(authException));
//输出
this.WriteJSON(request, response, respBean);
}
}
2、权限不足处理器
@Component
public class MyAccessDeniedHandler extends JSONAuthentication implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//用户权限不足
RespBean respBean = new RespBean(RespBeanEnum.USER_NO_PERMISSIONS);
//输出
this.WriteJSON(request, response, respBean);
}
}
3、SpringSecurity配置
在configure方法中配置失败处理器
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyOncePerRequestFilter myOncePerRequestFilter;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/doLogin").anonymous()
//其余url都要认证才能访问
.anyRequest().authenticated();
//3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
//4、异常处理
http
.exceptionHandling()
//认证失败处理器
.authenticationEntryPoint(myAuthenticationEntryPoint)
//权限不足处理器
.accessDeniedHandler(myAccessDeniedHandler);
}
}
八、跨域
1、编写配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
//允许任何域名
.allowedOriginPatterns("*")
//允许任何方法
.allowedMethods("PUT", "DELETE", "GET", "POST", "OPTIONS")
//允许任何头
.allowedHeaders("*")
//暴露头
.exposedHeaders("access-control-allow-headers",
"access-control-allow-methods",
"access-control-allow-origin",
"access-control-max-age",
"X-frame-Options")
// 是否允许证书(cookies)
.allowCredentials(true)
.maxAge(3600);
}
}
2、在SpringSecurity配置类中配置
在配置类的configure()方法中开启允许跨域
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyOncePerRequestFilter myOncePerRequestFilter;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/doLogin").anonymous()
//其余url都要认证才能访问
.anyRequest().authenticated();
//3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
//4、异常处理
http
.exceptionHandling()
//认证失败处理器
.authenticationEntryPoint(myAuthenticationEntryPoint)
//权限不足处理器
.accessDeniedHandler(myAccessDeniedHandler);
//5、允许跨域
http.cors();
}
}



