一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
我所学习的正是,认证和授权和流程。
另外默认SpringBoot、Redis会调用。
新建一个SpringBoot工程。这是使用的依赖。
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-securityartifactId>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-webartifactId>
- dependency>
-
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-testartifactId>
- <scope>testscope>
- dependency>
-
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-data-redisartifactId>
- dependency>
- <dependency>
- <groupId>com.baomidougroupId>
- <artifactId>mybatis-plus-boot-starterartifactId>
- <version>3.5.3.1version>
- dependency>
- <dependency>
- <groupId>mysqlgroupId>
- <artifactId>mysql-connector-javaartifactId>
- dependency>
- <dependency>
- <groupId>org.projectlombokgroupId>
- <artifactId>lombokartifactId>
- dependency>
- <dependency>
- <groupId>com.alibabagroupId>
- <artifactId>fastjsonartifactId>
- <version>1.2.58version>
- dependency>
- <dependency>
- <groupId>com.fasterxml.jackson.datatypegroupId>
- <artifactId>jackson-datatype-jdk8artifactId>
- dependency>
- <dependency>
- <groupId>io.jsonwebtokengroupId>
- <artifactId>jjwtartifactId>
- <version>0.9.1version>
- dependency>
- <dependency>
- <groupId>org.apache.commonsgroupId>
- <artifactId>commons-pool2artifactId>
- dependency>
-
-
- <dependency>
- <groupId>javax.xml.bindgroupId>
- <artifactId>jaxb-apiartifactId>
- <version>2.3.0version>
- dependency>
- <dependency>
- <groupId>com.sun.xml.bindgroupId>
- <artifactId>jaxb-implartifactId>
- <version>2.3.0version>
- dependency>
- <dependency>
- <groupId>com.sun.xml.bindgroupId>
- <artifactId>jaxb-coreartifactId>
- <version>2.3.0version>
- dependency>
- <dependency>
- <groupId>javax.activationgroupId>
- <artifactId>activationartifactId>
- <version>1.1.1version>
- dependency>
启动之后,访问localhost:8888, 出现一下页面代表SpringSecurity生效了。
此时我们发现控制台生成:
我们向登录表单输入,这段密码和用户名user,即可登录通过。
当然,我们实际情况,可能会加一个redis做缓存,登录之后,用用户id -> 用户信息,存储到redis中。
这样我们登录之后,解析token,就是从redis查询。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
1. ...,在现在前后端分离项目,我们肯定是要自定义登录接口,不能用SpringSecurity的登录页面;
2. UserDetailService 是在内存中查找比较用户输入的用户名和密码,那我们需要是需要查询数据库比对的。
user.sql
- DROP TABLE IF EXISTS `user`;
- CREATE TABLE `user` (
- `id` bigint NOT NULL COMMENT '主键',
- `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
- `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
- `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
- `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
- `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
- `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
- `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
- `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
- `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
- `create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',
- `create_time` datetime DEFAULT NULL COMMENT '创建时间',
- `update_by` bigint DEFAULT NULL COMMENT '更新人',
- `update_time` datetime DEFAULT NULL COMMENT '更新时间',
- `del_flag` int DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
application.yaml 中数据库配置
- spring:
- datasource:
- url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC
- username: root
- password: root
- driver-class-name: com.mysql.cj.jdbc.Driver
UserMapper
- @Mapper
- public interface UserMapper extends BaseMapper
{ - }
User
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- @TableName(value = "user")
- public class User {
-
- /**
- * 主键
- */
- @TableId
- private Long id;
- /**
- * 用户名
- */
- private String userName;
- /**
- * 昵称
- */
- private String nickName;
- /**
- * 密码
- */
- private String password;
- /**
- * 账号状态(0正常 1停用)
- */
- private String status;
- /**
- * 邮箱
- */
- private String email;
- /**
- * 手机号
- */
- private String phonenumber;
- /**
- * 用户性别(0男,1女,2未知)
- */
- private String sex;
- /**
- * 头像
- */
- private String avatar;
- /**
- * 用户类型(0管理员,1普通用户)
- */
- private String userType;
- /**
- * 创建人的用户id
- */
- private Long createBy;
- /**
- * 创建时间
- */
- private Date createTime;
- /**
- * 更新人
- */
- private Long updateBy;
- /**
- * 更新时间
- */
- private Date updateTime;
- /**
- * 删除标志(0代表未删除,1代表已删除)
- */
- private Integer delFlag;
- }
UserDetailServiceImpl
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public class UserDetailsImpl implements UserDetails {
-
- private User user;
-
- //private List
authList; - //
- //@JSONField(serialize = false)
- //private List
authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redis - //
- //
- //public UserDetailsImpl(User user, List
authList) { // 将对应的权限字符串列表传入 - // this.user = user;
- // this.authList = authList;
- //}
- //
-
-
- @Override
- public Collection extends GrantedAuthority> getAuthorities() {
- 初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;
- //if(authorities != null){
- // return authorities;
- //}else{
- // authorities = new ArrayList<>();
- //}
- //
- 第一次登录,封装UserDetails对象,初始化权限列表
- //for (String auth : authList) {
- // SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);
- // authorities.add(simpleGrantedAuthority); // 对,默认是个空的
- //}
- //return authorities;
-
- return null;
- }
-
- @Override
- public String getPassword() {
- return user.getPassword();
- }
-
- @Override
- public String getUsername() {
- return user.getUserName();
- }
-
- @Override
- public boolean isAccountNonExpired() { // 下面bool值,全部响应为true,UserDetail对象返回校验过程中,会因为没权限报错
- return true;
- }
-
- @Override
- public boolean isAccountNonLocked() {
- return true;
- }
-
- @Override
- public boolean isCredentialsNonExpired() {
- return true;
- }
-
- @Override
- public boolean isEnabled() {
- return true;
- }
- }
RedisConfig : redis配置类
- @Configuration
- public class RedisConfig {
-
- @Bean
- @SuppressWarnings(value = { "unchecked", "rawtypes" })
- public RedisTemplate
- {
- RedisTemplate
- template.setConnectionFactory(connectionFactory);
-
- FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
-
- // 使用StringRedisSerializer来序列化和反序列化redis的key值
- template.setKeySerializer(new StringRedisSerializer());
- template.setValueSerializer(serializer);
-
- // Hash的key也采用StringRedisSerializer的序列化方式
- template.setHashKeySerializer(new StringRedisSerializer());
- template.setHashValueSerializer(serializer);
-
- template.afterPropertiesSet();
- return template;
- }
- }
Result : 后端统一封装响应
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class Result
{ -
- private T data;
- private String mes;
- private Integer code;
-
- public Result(String mes, Integer _code) {
- }
-
-
- public static
Result success(T data){ - return new Result(data,"操作成功",200);
- }
-
- public static
Result success(String _mes){ - return new Result(_mes,200);
- }
-
- public static
Result error(String _mes,Integer _code){ - return new Result(_mes,_code);
- }
- }
JwtUtil 工具类
- /**
- * JWT工具类
- */
- public class JwtUtil {
-
- //有效期为
- public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
- //设置秘钥明文
- public static final String JWT_KEY = "sangeng";
-
- public static String getUUID(){
- String token = UUID.randomUUID().toString().replaceAll("-", "");
- return token;
- }
-
- /**
- * 生成jtw
- * @param subject token中要存放的数据(json格式)
- * @return
- */
- public static String createJWT(String subject) {
- JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
- return builder.compact();
- }
-
- /**
- * 生成jtw
- * @param subject token中要存放的数据(json格式)
- * @param ttlMillis token超时时间
- * @return
- */
- 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=JwtUtil.JWT_TTL;
- }
- long expMillis = nowMillis + ttlMillis;
- Date expDate = new Date(expMillis);
- return Jwts.builder()
- .setId(uuid) //唯一的ID
- .setSubject(subject) // 主题 可以是JSON数据
- .setIssuer("sg") // 签发者
- .setIssuedAt(now) // 签发时间
- .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
- .setExpiration(expDate);
- }
-
- /**
- * 创建token
- * @param id
- * @param subject
- * @param ttlMillis
- * @return
- */
- public static String createJWT(String id, String subject, Long ttlMillis) {
- JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
- return builder.compact();
- }
-
- public static void main(String[] args) throws Exception {
- // String jwt = createJWT("2123");
- Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");
- String subject = claims.getSubject();
- System.out.println(subject);
- // System.out.println(claims);
- }
-
- /**
- * 生成加密后的秘钥 secretKey
- * @return
- */
- public static SecretKey generalKey() {
- byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
- SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
- return key;
- }
-
- /**
- * 解析
- *
- * @param jwt
- * @return
- * @throws Exception
- */
- public static Claims parseJWT(String jwt) throws Exception {
- SecretKey secretKey = generalKey();
- return Jwts.parser()
- .setSigningKey(secretKey)
- .parseClaimsJws(jwt)
- .getBody();
- }
-
-
- }
FastJsonRedisSerializer
- public class FastJsonRedisSerializer
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);
- }
- }
我们自定义UserDatailService 实现UserDatailService接口,注入到spring容器中,这样就会在SpringSecurity的认证流程中调用我们自定义的实现类。
- @Service
- public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {
-
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- // 用用户名查询对应User
- LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(User::getUserName,username);
- // 判断是否空
- User user = this.getOne(queryWrapper);
- if(user == null){
- throw new RuntimeException("用户名或密码错误");
- }
- // 将查询到user封装到自定义UserDeatail中
- return new UserDetailsImpl(user);
- }
- }
我们看下图,我们输入的用户名和密码,最终会传入UserDetailService 对象,加载loadUserByUsername 方法,返回UserDtail对象。返回过程中,会与UserDetail中的password和username进行比对,不同则报错。
注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如 : 数据库中,username:sg, password: {noop}1234
这样登陆的时候就可以用sg作为用户名,1234作为密码来登陆了。
在实际中项目中,我一般不会明文存储,而是采用PasswordEncoder加密的方式;
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验
- @Configuration
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
-
-
- @Bean
- public PasswordEncoder passwordEncoder(){
- return new BCryptPasswordEncoder();
- }
-
- }
UserController
- @RestController
- @RequestMapping("/user")
- public class UserController {
-
- @Autowired
- private UserService userService;
-
- @PostMapping("/login")
- Result login(@RequestBody User user){
- return userService.login(user);
- }
-
- }
- public interface UserService extends IService
{ - Result login(User user);
- }
- @Service
- public class UserServiceImpl extends ServiceImpl
implements UserService { -
- @Autowired
- private AuthenticationManager authenticationManager;
-
- @Autowired
- private RedisTemplate redisTemplate;
-
-
-
- @Override
- public Result login(User user) {
- // AuthenticationManager authenticationManager 进行认证
- UsernamePasswordAuthenticationToken authenticationToken =
- new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
- Authentication authenticate = authenticationManager.authenticate(authenticationToken);
- // 如果认证通过、给出对应提示
- if(Objects.isNull(authenticate)){
- throw new RuntimeException("登录失败");
- }
- // 认证通过使用userid生成jwt
- UserDetailsImpl udi = (UserDetailsImpl) authenticate.getPrincipal();
- String userId = udi.getUser().getId().toString();
- String token = JwtUtil.createJWT(userId);
- HashMap
map = new HashMap<>(); - map.put("token",token);
- // 把完整用户信息存入redis
- redisTemplate.opsForValue().set("login:" +userId,udi);
- return Result.success(map,"登录成功");
- }
-
-
- }
SpringSecurityConfig
- @Configuration
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
-
-
- @Bean
- public PasswordEncoder passwordEncoder(){
- return new BCryptPasswordEncoder();
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http
- //关闭csrf
- .csrf().disable()
- //不通过Session获取SecurityContext
- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and()
- .authorizeRequests()
- // 对于登录接口 允许匿名访问
- .antMatchers("/user/login").anonymous()
- // 除上面外的所有请求全部需要鉴权认证
- .anyRequest().authenticated();
- }
-
- @Bean
- @Override
- public AuthenticationManager authenticationManagerBean() throws Exception {
- return super.authenticationManagerBean();
- }
- }
- spring:
- # mysql配置
- datasource:
- url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC
- username: root
- password: 123456
- driver-class-name: com.mysql.cj.jdbc.Driver
-
- # redis配置
- redis:
- # 默认0库
- database: 0
- #连接超时时间
- timeout: 10000ms
- port: 6379
- host: 192.168.213.136
- lettuce:
- pool:
- # 设置最大连接数
- max-active: 1024
- # 最大阻塞时间
- max-wait: 10000ms
- # 最大空间连接,默认8
- max-idle: 200
- # 最小空间连接,默认5
- min-idle: 5
- server:
- port: 8888
以下跨域设置,postman测试是完全没有问题的。但是,我们是为了前后端分离,会有问题。
当登录之后,携带token的请求头,被jwt过滤器捕获解析之后,获得userId,用userId将从redis拿出UserDetailImpl封装到 SecurityContextHolder.getContext()中,被SpringSecurity过滤链捕获到,证明没有问题,因此放行访问资源。
- /**
- * jwt过滤器
- *
- * @author: qhx20040819
- * @date: 2023-09-09 21:27
- **/
- @Component
- public class JwtFilter extends OncePerRequestFilter {
-
-
- @Autowired
- private RedisTemplate redisTemplate;
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- // 设置跨域
- response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); // 修改携带cookie,PS
- response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT");
- response.setHeader("Access-Control-Allow-Headers", "Authorization,content-type"); // PS
- // 预检请求缓存时间(秒),即在这个时间内相同的预检请求不再发送,直接使用缓存结果。
- response.setHeader("Access-Control-Max-Age", "3600");
-
- //获取token
- String authorization = request.getHeader("Authorization");
- if(StringUtils.isEmpty(authorization)){
- filterChain.doFilter(request,response);
- return;
- }
- String token = authorization.substring(6);
- //解析token
- String userid;
- try {
- Claims claims = JwtUtil.parseJWT(token);
- userid = claims.getSubject();
- } catch (Exception e) {
- e.printStackTrace();
- throw new RuntimeException("token非法");
- }
- //从redis中获取用户信息
- String redisKey = "login:" + userid;
- UserDetailsImpl udi = (UserDetailsImpl) redisTemplate.opsForValue().get(redisKey);
-
- if(Objects.isNull(udi)){
- throw new RuntimeException("用户未登录");
- }
- //存入SecurityContextHolder
- //获取权限信息封装到Authentication中
- UsernamePasswordAuthenticationToken authenticationToken = // 现在权限字段是null
- new UsernamePasswordAuthenticationToken(udi,null,udi.getAuthorities());
- SecurityContextHolder.getContext().setAuthentication(authenticationToken);
- //放行
- filterChain.doFilter(request, response);
- }
- }
在SpringSecurity中,会使用默认FiterSecuritInterceptor来进行权限校验,而它又是从SpringSecurityContex中Authentication,来获取其中的权限信息。判断当前用户是否拥有访问当前资源权限。
因此,我们只需要将权限信息存储到Authentication中即可,设置资源的访问权限。
用注解开启相关配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)
在相应的资源上开启访问权限管控。
- @RequestMapping("/hello")
- @PreAuthorize("hasAnyAuthority('test')") // 限制访问权限字段
- public String hello(){
- return "hello";
- }
我们之前UserDetailServiceImpl中loadUserByUsername()中,返回的UserDetailImpl对象,我们之前创建时,并没传入权限字段,我们先自定义权限字段模拟用户权限列表。
- @Service
- public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {
-
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- // 用用户名查询对应User
- LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(User::getUserName,username);
- // 判断是否空
- User user = this.getOne(queryWrapper);
- if(user == null){
- throw new RuntimeException("用户名或密码错误");
- }
-
- List
authlist = new ArrayList<>(); // 暂且封装这样权限字段 - authlist.add("test");
- authlist.add("test0");
-
- return new UserDetailsImpl(user,authlist);
- }
- }
相应的UserDetailImpl中也需要修改,seucrity过滤器链是从getAuthorities()方法中来获取用户权限字段的。
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public class UserDetailsImpl implements UserDetails {
-
- private User user;
-
- private List
authList; -
- @JSONField(serialize = false)
- private List
authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redis -
-
- public UserDetailsImpl(User user, List
authList) { // 将对应的权限字符串列表传入 - this.user = user;
- this.authList = authList;
- }
-
-
-
- @Override
- public Collection extends GrantedAuthority> getAuthorities() {
- // 初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;
- if(authorities != null){
- return authorities;
- }else{
- authorities = new ArrayList<>();
- }
-
- // 第一次登录,封装UserDetails对象,初始化权限列表
- for (String auth : authList) {
- SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);
- authorities.add(simpleGrantedAuthority); // 对,默认是个空的
- }
- return authorities;
-
- }
-
- @Override
- public String getPassword() {
- return user.getPassword();
- }
-
- @Override
- public String getUsername() {
- return user.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;
- }
- }
这是拿着携带token去请求,能访问成功。
就是我们真实的权限应该封装在数据库中。
一个用户,可以对应多个角色;一个角色,可以对应多个用户;
一个角色,可以拥有多个权限字段; 一个权限字段,可以被多个角色所拥有;
-
-
- USE `test`;
-
- /*Table structure for table `menu` */
-
- DROP TABLE IF EXISTS `menu`;
-
- CREATE TABLE `menu` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
- `path` varchar(200) DEFAULT NULL COMMENT '路由地址',
- `component` varchar(255) DEFAULT NULL COMMENT '组件路径',
- `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
- `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
- `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
- `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
- `create_by` bigint(20) DEFAULT NULL,
- `create_time` datetime DEFAULT NULL,
- `update_by` bigint(20) DEFAULT NULL,
- `update_time` datetime DEFAULT NULL,
- `del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
- `remark` varchar(500) DEFAULT NULL COMMENT '备注',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
-
- /*Table structure for table `role` */
-
- DROP TABLE IF EXISTS `role`;
-
- CREATE TABLE `role` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `name` varchar(128) DEFAULT NULL,
- `role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
- `status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
- `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
- `create_by` bigint(200) DEFAULT NULL,
- `create_time` datetime DEFAULT NULL,
- `update_by` bigint(200) DEFAULT NULL,
- `update_time` datetime DEFAULT NULL,
- `remark` varchar(500) DEFAULT NULL COMMENT '备注',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
-
- /*Table structure for table `role_menu` */
-
- DROP TABLE IF EXISTS `role_menu`;
-
- CREATE TABLE `role_menu` (
- `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
- `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
- PRIMARY KEY (`role_id`,`menu_id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-
- /*Table structure for table `user` */
-
- DROP TABLE IF EXISTS `user`;
-
- CREATE TABLE `user` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
- `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
- `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
- `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
- `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
- `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
- `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
- `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
- `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
- `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
- `create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
- `create_time` datetime DEFAULT NULL COMMENT '创建时间',
- `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
- `update_time` datetime DEFAULT NULL COMMENT '更新时间',
- `del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-
- /*Table structure for table `user_role` */
-
- DROP TABLE IF EXISTS `user_role`;
-
- CREATE TABLE `user_role` (
- `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
- `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
- PRIMARY KEY (`user_id`,`role_id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- SELECT
- DISTINCT m.`perms`
- FROM
- user_role ur
- LEFT JOIN `role` r ON ur.`role_id` = r.`id`
- LEFT JOIN `role_menu` rm ON ur.`role_id` = rm.`role_id`
- LEFT JOIN `menu` m ON m.`id` = rm.`menu_id`
- WHERE
- user_id = 2
- AND r.`status` = 0
- AND m.`status` = 0
- @TableName(value="menu")
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- @JsonInclude(JsonInclude.Include.NON_NULL)
- public class Menu implements Serializable {
- private static final long serialVersionUID = -54979041104113736L;
-
- @TableId
- private Long id;
- /**
- * 菜单名
- */
- private String menuName;
- /**
- * 路由地址
- */
- private String path;
- /**
- * 组件路径
- */
- private String component;
- /**
- * 菜单状态(0显示 1隐藏)
- */
- private String visible;
- /**
- * 菜单状态(0正常 1停用)
- */
- private String status;
- /**
- * 权限标识
- */
- private String perms;
- /**
- * 菜单图标
- */
- private String icon;
-
- private Long createBy;
-
- private Date createTime;
-
- private Long updateBy;
-
- private Date updateTime;
- /**
- * 是否删除(0未删除 1已删除)
- */
- private Integer delFlag;
- /**
- * 备注
- */
- private String remark;
- }
- @Mapper
- public interface MenuMapper extends BaseMapper
-
- List
selectPermsByUserId(Long userId); - }
- "1.0" encoding="UTF-8"?>
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.qhx.springsecuritydemo.mapper.MenuMapper">
- <select id="selectPermsByUserId" resultType="java.lang.String">
- SELECT
- m.perms
- FROM
- `user_role` as ur
- LEFT JOIN `role` as r on ur.role_id = r.id
- LEFT JOIN `role_menu` as rm on ur.role_id = rm.role_id
- LEFT JOIN `menu` as m on rm.menu_id = m.id
- WHERE
- ur.`user_id` = #{userId} AND r.`status`=0 and m.`status`=0;
- select>
-
-
- mapper>
修改UserDetailServiceImpl。
- @Service
- public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {
-
- @Autowired
- private MenuMapper menuMapper;
-
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- // 用用户名查询对应User
- LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(User::getUserName,username);
- // 判断是否空
- User user = this.getOne(queryWrapper);
- if(user == null){
- throw new RuntimeException("用户名或密码错误");
- }
-
- List
authList = menuMapper.selectPermsByUserId(user.getId()); -
- return new UserDetailsImpl(user,authlist);
- }
- }
我们发现,就算我们登录输入的用户名和错误的:
1.SpringSecurity没有抛出异常 2.也有没有相关响应提示信息;
我是并不希望这样,因此要添加自定义异常处理器:
- /**
- * 授权异常处理器
- **/
- @Component
- public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
- @Override
- public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
-
- Result result = Result.error("权限不足", 403);
- String jsonResult = JSON.toJSONString(result);
-
- response.setStatus(200);
- response.setContentType("application/json");
- response.getWriter().write(jsonResult);
-
- }
- }
- /**
- * 认证异常处理器
- **/
- @Component
- public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
-
- @Override
- public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
-
- Result result = Result.error("您认证错误/请检查你的用户名或密码是否正确", 401);
- String jsonResult = JSON.toJSONString(result);
-
- response.setStatus(200);
- response.setContentType("application/json");
- response.getWriter().write(jsonResult);
-
- }
- }
在SecurityConfig配置中引入
- @Configuration
- @EnableGlobalMethodSecurity(prePostEnabled = true)
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
-
- @Autowired
- private JwtFilter jwtFilter;
-
-
- @Bean
- public PasswordEncoder passwordEncoder(){
- return new BCryptPasswordEncoder();
- }
-
- @Autowired
- private AccessDeniedHandlerImpl accessDeniedHandler;
- @Autowired
- private AuthenticationEntryPoint authenticationEntryPoint;
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http
- //关闭csrf
- .csrf().disable()
- //不通过Session获取SecurityContext
- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and()
- .authorizeRequests()
- // 对于登录接口 允许匿名访问
- .antMatchers("/user/login").anonymous()
- // 除上面外的所有请求全部需要鉴权认证
- .anyRequest().authenticated();
-
- // 添加自定义token过滤器到链中
- http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
-
- // 添加自定义异常处理器
- http.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
- .authenticationEntryPoint(authenticationEntryPoint);
- }
-
- @Bean
- @Override
- public AuthenticationManager authenticationManagerBean() throws Exception {
- return super.authenticationManagerBean();
- }
- }
上面除了我们SpringSecurity异常,那么还有一些其他异常,我们不希望每次捕获,并手动抛出,因此可以转化为自定义异常,用SpringBoot的全局异常处理器捕获并且抛出;
我们现在是用postman测试,但是未来项目涉及到前后端分离项目交互的,都伴随着跨域的问题。
后端能接受到前端原生request对象,response对象,都涉及到跨域的问题。
SpringSecurity解决跨域
SringMvc解决跨域
- @Configuration
- public class CorsConfig implements WebMvcConfigurer {
-
- @Override
- public void addCorsMappings(CorsRegistry registry) {
- // 设置允许跨域的路径
- registry.addMapping("/**")
- // 设置允许跨域请求的域名
- .allowedOriginPatterns("*")
- // 是否允许cookie
- .allowCredentials(true)
- // 设置允许的请求方式
- .allowedMethods("GET", "POST", "DELETE", "PUT")
- // 设置允许的header属性
- .allowedHeaders("*")
- // 跨域允许时间
- .maxAge(3600);
- }
- }
自定义jwt过滤器解决跨域