基于SpringBoot2.7+SpringSecurity5.7的登录鉴权方案

添加配置类SecurityConfig

SpringSecurity5.7+不需要继承WebSecurityConfigurerAdapter,而是注入一个过滤链的Bean,通过这个过滤链去处理用户登录的请求

@Bean
SecurityFilterChain filerChain(HttpSecurity http) throws Exception {
    return http.authorizeHttpRequests()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable()
                .build();
}

自定义登录处理

登陆成功

  • 自定义AuthenticationSuccessHandler接口的实现类

    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("msg", "登录成功");
            result.put("status", 200);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);
        }
    }
    
  • filterChain方法中替换

    .and().formLogin().successHandler(new MyAuthenticationSucccessHandler())
    

登陆失败

  • 自定义AuthenticationFailureHandler接口的实现类

    public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("msg", "登录失败: "+exception.getMessage());
            result.put("status", 500);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);
        }
    }
    
  • filterChain方法中替换

    .failureHandler(new MyAuthenticationFailureHandler())
    

自定义登出处理

  • 自定义LogoutSuccessHandler接口的实现类

    public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("msg", "注销成功");
            result.put("status", 200);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);
        }
    }
    
  • filterChain方法中替换

    .and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
    
    • 通过 logout() 方法开启注销配置

    • logoutUrl 指定退出登录请求地址,默认是 GET 请求,路径为 /logout

    • invalidateHttpSession 退出时是否使 session 失效,默认值为 true

    • clearAuthentication 退出时是否清除认证信息,默认值为 true

    • 可以配置多个注销登录的请求,指定方法

      .logoutRequestMatcher(new OrRequestMatcher(
                              new AntPathRequestMatcher("/logout1","GET"),
                              new AntPathRequestMatcher("/logout","GET")
                      ))
      

自定义认证数据源

  • 依赖

    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.2.2</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.2.11</version>
    </dependency>
    
  • 设计表结构

    -- 用户表
    CREATE TABLE `user`
    (
        `id`                    int(11) NOT NULL AUTO_INCREMENT,
        `username`              varchar(32)  DEFAULT NULL,
        `password`              varchar(255) DEFAULT NULL,
        `enabled`               tinyint(1) DEFAULT NULL,
        `accountNonExpired`     tinyint(1) DEFAULT NULL,
        `accountNonLocked`      tinyint(1) DEFAULT NULL,
        `credentialsNonExpired` tinyint(1) DEFAULT NULL,
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
    -- 角色表
    CREATE TABLE `role`
    (
        `id`      int(11) NOT NULL AUTO_INCREMENT,
        `name`    varchar(32) DEFAULT NULL,
        `name_zh` varchar(32) DEFAULT NULL,
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
    -- 用户角色关系表
    CREATE TABLE `user_role`
    (
        `id`  int(11) NOT NULL AUTO_INCREMENT,
        `uid` int(11) DEFAULT NULL,
        `rid` int(11) DEFAULT NULL,
        PRIMARY KEY (`id`),
        KEY   `uid` (`uid`),
        KEY   `rid` (`rid`)
    ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
    
  • 加配置

    # datasource
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/demo4_springsecurity
    spring.datasource.username=root
    spring.datasource.password=123456
    
    # mybatis
    mybatis.mapper-locations=classpath:com/demo/mapper/*.xml
    mybatis.type-aliases-package=com.demo.entity
    
    # log
    logging.level.com.demo=debug
    
    
  • 做service,dao,mapper,entity

    • Role类

      package com.demo.entity;
      
      import lombok.AllArgsConstructor;
      import lombok.Data;
      import lombok.NoArgsConstructor;
      
      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class Role {
          private Integer id;
          private String name;
          private String nameZh;
          //get set..
      }
      
    • User类

      package com.demo.entity;
      
      import org.springframework.security.core.GrantedAuthority;
      import org.springframework.security.core.authority.SimpleGrantedAuthority;
      import org.springframework.security.core.userdetails.UserDetails;
      
      import java.util.ArrayList;
      import java.util.Collection;
      import java.util.List;
      
      public class User  implements UserDetails {
          private Integer id;
          private String username;
          private String password;
          private Boolean enabled;
          private Boolean accountNonExpired;
          private Boolean accountNonLocked;
          private Boolean credentialsNonExpired;
          private List<Role> roles = new ArrayList<>();
      
          @Override
          public Collection<? extends GrantedAuthority> getAuthorities() {
              List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
              roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
              return grantedAuthorities;
          }
      
          @Override
          public String getPassword() {
              return password;
          }
      
          @Override
          public String getUsername() {
              return username;
          }
      
          @Override
          public boolean isAccountNonExpired() {
              return accountNonExpired;
          }
      
          @Override
          public boolean isAccountNonLocked() {
              return accountNonLocked;
          }
      
          @Override
          public boolean isCredentialsNonExpired() {
              return credentialsNonExpired;
          }
      
          public Integer getId() {
              return id;
          }
      
          public void setRoles(List<Role> roles) {
              this.roles = roles;
          }
          
          public List<Role> getRoles(){ return this.roles; }
          
          @Override
          public boolean isEnabled() {
              return enabled;
          }
          //get/set....
          public void setPassword(String password) {
              this.password = password;
          }
      }
      
    • UserDao

      package com.demo.dao;
      
      import com.demo.entity.Role;
      import com.demo.entity.User;
      import org.apache.ibatis.annotations.Mapper;
      import java.util.List;
      
      @Mapper
      public interface UserDao {
          //根据用户名查询用户
          User loadUserByUsername(@Param("username") String username);
      
          //根据用户id查询角色
          List<Role> getRolesByUid(@Param("uid") Integer uid);
      }
      
    • UserMapper

      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.demo.dao.UserDao">
          <!--查询单个-->
          <select id="loadUserByUsername" resultType="com.demo.entity.User">
              select id,
                     username,
                     password,
                     enabled,
                     accountNonExpired,
                     accountNonLocked,
                     credentialsNonExpired
              from user
              where username = #{username}
          </select>
      
          <!--查询指定行数据-->
          <select id="getRolesByUid" resultType="com.demo.entity.Role">
              select r.id,
                     r.name,
                     r.name_zh nameZh
              from role r,
                   user_role ur
              where r.id = ur.rid
                and ur.uid = #{uid}
          </select>
      </mapper>
      
    • UserDetailService

      package com.demo.service;
      
      import com.demo.dao.UserDao;
      import com.demo.entity.User;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.core.userdetails.UsernameNotFoundException;
      import org.springframework.stereotype.Service;
      import org.springframework.util.ObjectUtils;
      
      @Service
      public class MyUserDetailServiceImpl implements UserDetailsService {
      
          private  final UserDao userDao;
      
          @Autowired
          public MyUserDetailServiceImpl(UserDao userDao) {
              this.userDao = userDao;
          }
      
          @Override
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
              User user = userDao.loadUserByUsername(username);
              if(ObjectUtils.isEmpty(user)) {
                  throw new RuntimeException("用户不存在");
              }
              user.setRoles(userDao.getRolesByUid(user.getId()));
              return user;
          }
      }
      
  • 自定义LoginFilter继承UserNamePasswordFilter

    package com.demo.filter;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Map;
    
    public class LoginFilter extends UsernamePasswordAuthenticationFilter {
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            if (!request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
                try {
                    Map<String, String> userinfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                    String username = userinfo.get(getUsernameParameter());
                    String password = userinfo.get(getPasswordParameter());
                    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                    setDetails(request, authRequest);
                    return this.getAuthenticationManager().authenticate(authRequest);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return super.attemptAuthentication(request,response);
        }
    }
    
  • 配置SecurityConfig

        private final MyUserDetailServiceImpl myUserDetailService;
    
    	//构造注入自定义UserDetailService
        @Autowired
        public SecurityConfig(MyUserDetailServiceImpl myUserDetailService) {
            this.myUserDetailService = myUserDetailService;
        }
    	//注入AuthenticationManager
        @Bean
        public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
            return configuration.getAuthenticationManager();
        }
    	//注入过滤器
        @Bean
        public LoginFilter loginFilter(AuthenticationManager authenticationManager){
            LoginFilter loginFilter = new LoginFilter();
            loginFilter.setPasswordParameter("password");
            loginFilter.setUsernameParameter("username");
            loginFilter.setFilterProcessesUrl("/login");
            loginFilter.setAuthenticationManager(authenticationManager);
            loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSucccessHandler());
            loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
            return loginFilter;
        }
    	//配置添加自定义认证数据源,替换过滤器
        @Bean
        protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .anyRequest().authenticated()
                    .and().formLogin()
                    .and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
                    .and().userDetailsService(myUserDetailService)
                    .csrf().disable();
            http.addFilterAt(loginFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class);
            return http.build();
        }
    

密码加密并支持旧密码自动升级

下次用户登录时密码自动升级为Bcrypt加密

  • 自定义UserDetailService新增实现接口UserDetailsPasswordService

    @Override
        public UserDetails updatePassword(UserDetails user, String newPassword) {
            Integer result = userDao.updatePassword(user.getUsername(), newPassword);
            if (result == 1) {
                ((User) user).setPassword(newPassword);
            }
            return user;
        }
    
  • userDao和userMapper新增对应实现

    • userDao

      Integer updatePassword(@Param("username") String username,@Param("password") String password);
      
    • userMapper

      <update id="updatePassword">
              update `user` set password=#{password}
              where username=#{username}
          </update>
      

基于JWT验证的登录

JWT工具类

public class JWTUtils {
    public static String TOKEN_HEADER = "token";

    //过期时间
    private static int EXPIRITION_DAY =7;

    private static String ROLE = "role";

    private static String SIGN = "#N!&SI#^";

    public static String createToken(String username, List<Role> roles){
        ArrayList<String> rolesList = new ArrayList<>();
        for(Role role:roles){
            rolesList.add(role.getName());
        }
        Calendar instance = Calendar.getInstance();
        //过期时间设为7天
        instance.add(Calendar.DATE,EXPIRITION_DAY);
        String token = JWT.create()
                .withArrayClaim("role",rolesList.toArray(new String[0]))
                .withClaim("username",username)
                .withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256(SIGN));
        return token;
    }

    public static HashMap<String,Object> decode(String token){
        HashMap<String, Object> map = new HashMap<>();
        DecodedJWT verify;
        try{
            verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
        }catch (Exception e){
            e.printStackTrace();
            return map;
        }
        String username = verify.getClaim("username").asString();
        String[] roles = verify.getClaim("role").asArray(String.class);
        map.put("username",username);
        map.put("roles",roles);
        return map;
    }

    public static void setExpiritionDay(int expiritionDay) {
        EXPIRITION_DAY = expiritionDay;
    }

    public static void setRole(String role) {
        JWTUtils.ROLE = role;
    }

    public static void setSign(String sign) {
        JWTUtils.SIGN = sign;
    }
}

JWTAuthenticationFilter登录过滤器

基于之前实现的LoginFilter,在登陆成功处理中获取用户数据,返回Token

登陆成功处理Handler添加

User user = (User) authentication.getPrincipal();
String token = JWTUtils.createToken(user.getUsername(), user.getRoles());
result.put("token",token);

JWTAuthorizationFilter权限校验过滤器

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //从请求头中获取token
        String token = request.getHeader(JWTUtils.TOKEN_HEADER);
        //没有直接跳过过滤器
        if(ObjectUtils.isEmpty(token)){
            chain.doFilter(request,response);
            return;
        }
        //将token中的用户名和权限用户组放入Authentication对象,在之后实现鉴权
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(token));
        super.doFilterInternal(request, response, chain);
    }
    
    //解析token获取用户信息
    private UsernamePasswordAuthenticationToken getAuthentication(String token){
        HashMap<String, Object> tokenInfo = JWTUtils.decode(token);
        if(ObjectUtils.isEmpty(tokenInfo)){
            return null;
        }
        String username = (String) tokenInfo.get("username");
        String[] roles = (String[]) tokenInfo.get("roles");
        ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for(String role:roles){
            authorities.add(new SimpleGrantedAuthority(role));
        }
        return new UsernamePasswordAuthenticationToken(username,null,authorities);

    }
}

主类配置认证过滤器bean,添加过滤器

@Bean
    public JWTAuthorizationFilter jwtAuthorizationFilter(AuthenticationManager authenticationManager){
        JWTAuthorizationFilter filter = new JWTAuthorizationFilter(authenticationManager);
        return filter;
    }
http.addFilter(jwtAuthorizationFilter(http.getSharedObject(AuthenticationManager.class)));

设置不开启session

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

添加验证码

基于Redis

新增字段uuid和kaptcha

验证码和Redis依赖包

<dependency>
  <groupId>com.github.penggle</groupId>
  <artifactId>kaptcha</artifactId>
  <version>2.3.2</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

Redis配置

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-wait=100

验证码配置类

@Configuration
public class KaptchaConfig {
    @Bean
    public Producer kaptcha() {
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "150");
        properties.setProperty("kaptcha.image.height", "50");
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789qazwsxedcrfvtgbyhnujmikolpQAZWSXEDCRFVTGBYHNUJMIKOLP");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

获取验证码控制器

@RestController
public class KaptchaController {

    private final String redisParam = "demo:verifyCode:uuid:";
    private final Producer producer;

    private final StringRedisTemplate stringRedisTemplate;

    @Autowired
    public KaptchaController(Producer producer,StringRedisTemplate stringRedisTemplate) {
        this.producer = producer;
        this.stringRedisTemplate=stringRedisTemplate;
    }

    @RequestMapping("/vc.png")
    public HashMap<String,String> getVerifyCode() throws IOException {
        //1.生成验证码和标识UUID
        String code = producer.createText();
        String uuid = UUID.randomUUID().toString();
        //redis存放验证码,UUID,过期时长五分钟
        stringRedisTemplate.opsForValue().set(redisParam+uuid,code,5L, TimeUnit.MINUTES);
        BufferedImage bi = producer.createImage(code);
        //2.写入内存
        FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
        ImageIO.write(bi, "png", fos);
        System.out.println(code);
        //3.生成 base64,返回数据
        HashMap<String, String> ret = new HashMap<>();
        ret.put("uuid",uuid);
        ret.put("picture",Base64.encodeBase64String(fos.toByteArray()));
        return ret;
    }
}

验证码异常类

public class KaptchaNotMatchException extends AuthenticationException {

    public KaptchaNotMatchException(String msg) {
        super(msg);
    }

    public KaptchaNotMatchException(String msg, Throwable cause) {
        super(msg, cause);
    }
}

在登录验证过滤器实现校验验证码

以下为JWT登录校验的实现

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final String redisParam = "demo:verifyCode:uuid:";

    public static final String FORM_KAPTCHA_KEY = "kaptcha";

    public static final String FORM_UUID_KEY = "uuid";
    private String kaptcha=FORM_KAPTCHA_KEY;

    private String uuid = FORM_UUID_KEY;

    public String getuuidParamter() {
        return uuid;
    }

    public void setKaptchaParamter(String kaptcha) {
        this.kaptcha = kaptcha;
    }

    public void setuuidParamter(String uuid) {
        this.uuid = uuid;
    }

    public String getKaptchaParamter() {
        return kaptcha;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
            try {
                Map<String, String> userinfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userinfo.get(getUsernameParameter());
                String password = userinfo.get(getPasswordParameter());
                String kaptcha = userinfo.get(getKaptchaParamter());
                String uuid = userinfo.get(getuuidParamter());
                String kaptchaRedis = stringRedisTemplate.opsForValue().get(redisParam + uuid);
                if(!ObjectUtils.isEmpty(kaptchaRedis)&&!ObjectUtils.isEmpty(kaptcha)&&kaptchaRedis.equals(kaptcha)){
                    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                    setDetails(request, authRequest);
                    return this.getAuthenticationManager().authenticate(authRequest);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        throw new KaptchaNotMatchException("验证码错误或过期");
    }
}

实现Remember-me

  • LoginFilter新增rememberme字段

    String rememberValue = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
                    if (!ObjectUtils.isEmpty(rememberValue)) {
                        request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberValue);
                    }
    
  • 自定义RemembermeService实现类

    public class PersistentTokenBasedRememberMeServicesImpl extends PersistentTokenBasedRememberMeServices {
        public PersistentTokenBasedRememberMeServicesImpl(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
            super(key, userDetailsService, tokenRepository);
        }
        @Override
        protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
            String paramValue = request.getAttribute(parameter).toString();
            if (paramValue != null) {
                if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
                        || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
                    return true;
                }
            }
            return false;
        }
    }
    
  • 配置SecurityConfig

    	//持久化到数据库	
    	@Autowired
        private DataSource dataSource;
        @Bean
        public PersistentTokenRepository persistentTokenRepository(){
            JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
            jdbcTokenRepository.setCreateTableOnStartup(false);
            //只需要没有表时设置为 true
            jdbcTokenRepository.setDataSource(dataSource);
            return jdbcTokenRepository;
        }
    	//设置默认的remembermeService
    	@Bean
        public RememberMeServices rememberMeServices() {
            return new PersistentTokenBasedRememberMeServicesImpl(UUID.randomUUID().toString(), myUserDetailService, persistentTokenRepository());
        }
    
    • http配置添加

      .rememberMe().rememberMeServices(rememberMeServices())
      
    • LoginFilter配置添加

      loginFilter.setRememberMeServices(rememberMeServices());
      
    • Remember-me数据库表也可以手动创建

      create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
      

开启会话管理

基于JWT进行登录鉴权不需要

  • 会话过期处理

    public class MyInvalidSessionStrategy implements InvalidSessionStrategy {
        @Override
        public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException {
            response.setContentType("application/json;charset=UTF-8");
            Map<String, Object> result = new HashMap<>();
            result.put("status", HttpStatus.UNAUTHORIZED.value());
            result.put("msg", "当前会话已经过期,请重新登录");
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);
        }
    }
    
  • http配置添加

    .and().sessionManagement().invalidSessionStrategy(new MyInvalidSessionStrategy())
    
  • 配置类注入

    @Bean
        public HttpSessionEventPublisher httpSessionEventPublisher() {
            return new HttpSessionEventPublisher();
        }
    
  • 会话并发管理由于自定义过滤器和session不一致问题暂无解决

跨域配置

  • 配置类配置跨域

    CorsConfigurationSource configurationSource() {
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
            corsConfiguration.setAllowedMethods(Arrays.asList("*"));
            corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
            corsConfiguration.setMaxAge(3600L);
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", corsConfiguration);
            return source;
        }
    
  • http开启跨域

    .cors().configurationSource(configurationSource());
    

自定义异常处理

  • http配置添加**.exceptionHandling()**,在之后进行配置

未登录处理

配置**.authenticationEntryPoint()**

实现类:

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "未登录");
        result.put("status", 401);
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

权限异常处理

配置**.accessDeniedHandler()**

实现类:

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException{
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "权限不足 ");
        result.put("status", 403);
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

授权

基于URL

在http配置中的**authorizeHttpRequests()**后配置

.antMatchers("/*").hasRole("ADMIN")

权限表达式

方法说明
hasAuthority(String authority)当前用户是否具备指定权限
hasAnyAuthority(String… authorities)当前用户是否具备指定权限中任意一个
hasRole(String role)当前用户是否具备指定角色
hasAnyRole(String… roles);当前用户是否具备指定角色中任意一个
permitAll();放行所有请求/调用
denyAll();拒绝所有请求/调用
isAnonymous();当前用户是否是一个匿名用户
isAuthenticated();当前用户是否已经认证成功
isRememberMe();当前用户是否通过 Remember-Me 自动登录
isFullyAuthenticated();当前用户是否既不是匿名用户又不是通过 Remember-Me 自动登录的
hasPermission(Object targetId, Object permission);当前用户是否具备指定目标的指定权限信息
hasPermission(Object targetId, String targetType, Object permission);当前用户是否具备指定目标的指定权限信息

基于方法

配置类注解**@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true, jsr250Enabled=true)**

  • perPostEnabled: 开启 Spring Security 提供的四个权限注解,@PostAuthorize、@PostFilter、@PreAuthorize 以及@PreFilter。
  • securedEnabled: 开启 Spring Security 提供的 @Secured 注解支持,该注解不支持权限表达式
  • jsr250Enabled: 开启 JSR-250 提供的注解,主要是@DenyAll、@PermitAll、@RolesAll 同样这些注解也不支持权限表达式

以上注解含义如下

  • @PostAuthorize: 在目前标方法执行之后进行权限校验。
  • @PostFiter: 在目标方法执行之后对方法的返回结果进行过滤。
  • @PreAuthorize:在目标方法执行之前进行权限校验。
  • @PreFiter:在目前标方法执行之前对方法参数进行过滤。
  • @Secured:访问目标方法必须具各相应的角色。
  • @DenyAll:拒绝所有访问。
  • @PermitAll:允许所有访问。
  • @RolesAllowed:访问目标方法必须具备相应的角色。

在对应Controller方法或类上添加注解,如果数据库里存的权限是ROLE_admin,则基于url只需要admin,基于方法需要写全ROLE_admin

	@Secured("ROLE_admin")
    @RequestMapping("/t")
    public String t(HttpServletRequest request){
        return "111";
    }

其他用法

@PreAuthorize("hasRole('ADMIN') and authentication.name=='root'")
@PreAuthorize("authentication.name==#name")
@PreFilter(value = "filterObject.id%2!=0",filterTarget = "users")
@PostAuthorize("returnObject.id==1")
@PostFilter("filterObject.id%2==0")
@Secured({"ROLE_ADMIN","ROLE_USER"}) //具有其中一个即可
@RolesAllowed({"ROLE_ADMIN","ROLE_USER"}) //具有其中一个角色即可

OAuth2第三方登录

在对应开发者平台申请appid,appsecret

SpringSecurity通过OAuth2AuthorizationRequestRedirectFilter做第三方认证重定向,数据用Session保存,之后在OAuth2LoginAuthenticationFilter拦截用户同意登录后回调的地址,获取用户信息。

由于token验证是无状态式的,关闭了session,所以需要对oauth2登录完以后再生成我们的token用以鉴定是否登录,此处重写登录成功Handler

前端请求/oauth2/authorization/gitee,后端自动为用户重定向到gitee认证,用户认证后重定向请求后端/login/oauth2/code/gitee,获取code,后端再去第三方授权服务器拿token,再用token去拿info

从gitee获取的用户信息样例

{gists_url=https://gitee.com/api/v5/users/Loli_Wolf/gists{/gist_id}, repos_url=https://gitee.com/api/v5/users/Loli_Wolf/repos, following_url=https://gitee.com/api/v5/users/Loli_Wolf/following_url{/other_user}, bio=null, created_at=2022-11-13T12:41:11+08:00, remark=, login=Loli_Wolf, type=User, blog=https://github.com/LoliWolf, subscriptions_url=https://gitee.com/api/v5/users/Loli_Wolf/subscriptions, weibo=null, updated_at=2022-12-06T20:00:45+08:00, id=11995789, public_repos=0, email=null, organizations_url=https://gitee.com/api/v5/users/Loli_Wolf/orgs, starred_url=https://gitee.com/api/v5/users/Loli_Wolf/starred{/owner}{/repo}, followers_url=https://gitee.com/api/v5/users/Loli_Wolf/followers, public_gists=0, url=https://gitee.com/api/v5/users/Loli_Wolf, received_events_url=https://gitee.com/api/v5/users/Loli_Wolf/received_events, watched=0, followers=0, avatar_url=https://gitee.com/assets/no_portrait.png, events_url=https://gitee.com/api/v5/users/Loli_Wolf/events{/privacy}, html_url=https://gitee.com/Loli_Wolf, following=0, name=Loli_Wolf, stared=1}

oauth2配置样例

spring.security.oauth2.client.registration.gitee.client-id=
spring.security.oauth2.client.registration.gitee.client-secret= 
spring.security.oauth2.client.registration.gitee.redirect-uri=http://localhost:8080/login/oauth2/code/gitee
spring.security.oauth2.client.registration.gitee.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.gitee.client-authentication-method=POST
spring.security.oauth2.client.registration.gitee.client-name=gitee
spring.security.oauth2.client.registration.gitee.provider=gitee

spring.security.oauth2.client.provider.gitee.authorization-uri=https://gitee.com/oauth/authorize
spring.security.oauth2.client.provider.gitee.user-info-authentication-method=GET
spring.security.oauth2.client.provider.gitee.token-uri=https://gitee.com/oauth/token
spring.security.oauth2.client.provider.gitee.user-info-uri=https://gitee.com/api/v5/user
spring.security.oauth2.client.provider.gitee.user-name-attribute=name

HttpSecurity配置里开启oauth2

.and().oauth2Login().successHandler(new OAuth2AuthenticationSuccessHandler(userDetailService)).failureHandler(new MyAuthenticationFailureHandler())

UserDetailService新增Oauth2用户处理

如果没有用户就创建,否则返回绑定用户

public User loadUserByGiteeID(String id, String name){
        User user = userDao.loadUserByGiteeID(id);
        //新用户
        if (ObjectUtils.isEmpty(user)){
            try {
                String username = name + UUID.randomUUID();
                userDao.createUserByUsername(username);
                userDao.addUserGiteeInfoByUsername(username,id,name);
                userDao.addUserRoleByUsername(username,"ROLE_user");
                user = userDao.loadUserByGiteeID(id);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        user.setRoles(userDao.getRolesByUid(user.getId()));
        return user;
    }

新增登陆成功控制器

public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final MyUserDetailServiceImpl userDetailsService;

    public OAuth2AuthenticationSuccessHandler(MyUserDetailServiceImpl myUserDetailService) {
        this.userDetailsService = myUserDetailService;
    }

    //拿着第三方登录的用户去找user,找到就返回user生成的token,没找到就创建新user,设置user角色

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登录成功");
        result.put("status", 200);
        DefaultOAuth2User oAuth2User = (DefaultOAuth2User) authentication.getPrincipal();
        User user = userDetailsService.loadUserByGiteeID(oAuth2User.getAttributes().get("id").toString(),oAuth2User.getAttributes().get("login").toString());
        String token = JWTUtils.createToken(user.getUsername(), user.getRoles());
        result.put("token",token);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

UserMapper,UserDao新增sql

<insert id="createUserByUsername">
        insert into user(username, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired) value (#{username},1,1,1,1);
    </insert>
    <insert id="addUserGiteeInfoByUsername">
        insert into user_gitee value ((select id from user where username = #{username}),#{giteeID},#{giteeName});
    </insert>
    <insert id="addUserRoleByUsername">
        insert into user_role(uid,rid) value ((select id from user where username=#{username}),(select id from role where name = #{role}));
    </insert>
    <select id="loadUserByGiteeID" resultType="com.demo.security.entity.User">
        select user.* from user,user_gitee ug where #{giteeID} = ug.gitee_id and ug.uid = user.id;
    </select>
//根据giteeid查用户
    User loadUserByGiteeID(@Param("giteeID") String giteeID);
    
    //根据用户名创建新用户
    Integer createUserByUsername(@Param("username") String username);

    //绑定用户和gitee信息
    Integer addUserGiteeInfoByUsername(@Param("username") String username,@Param("giteeID") String giteeID,@Param("giteeName") String giteeName);

    //按用户名添加角色
    Integer addUserRoleByUsername(@Param("username") String username,@Param("role") String role);
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值