Spring-Security+OAuth2+redis implements password login

1. OAuth2 authentication mode

        There are four authentication methods in total, authorization code mode, password mode, simplified mode and client mode. To achieve single sign-on, the more popular method is to use the jwt method, jwt is stateless, qit itself can carry information, so the server does not need to save his information, but as long as the token does not expire, the user can always access, so The exit function cannot be realized. If you want to implement the logout function, you need to store token information on the server side, which violates the original intention of using jwt authentication.

2. Create a Security configuration class

        Inject the required tool classes and configure release and authentication rules in this class

/**
 * SecurityConfiguration 配置类
 */
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
//    注入Redis连接工厂
    @Resource
    private RedisConnectionFactory redisConnectionFactory;

//    Redis仓库类,初始化RedisTokenStore用于将Token存储到Redis
    @Bean
    public RedisTokenStore redisTokenStore(){
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        redisTokenStore.setPrefix("TOKEN:"); //设置KEY的层级前缀,方便查询
        return redisTokenStore;
    }

//    初始化密码编辑器,用MD5加密密码
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new PasswordEncoder() {
            /**
             * 加密
             * @param rawPassword 原始密码
             * @return
             */
            @Override
            public String encode(CharSequence rawPassword) {
                return DigestUtil.md5Hex(rawPassword.toString());
            }

            /**
             * 校验密码
             * @param rawPassword 原始密码
             * @param encodedPassword 加密密码
             * @return
             */
            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return DigestUtil.md5Hex(rawPassword.toString()).equals(encodedPassword);
            }
        };
    }

//    初始化认证管理对象,密码登录方式需要用到
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

//    放行和认证规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // csrf认为除了get以外的请求都是不安全的,禁用
        http.csrf().disable()
                .authorizeRequests()
//                放行的请求
                .antMatchers("/oauth/**", "/actuator/**").permitAll()
                .and()
                .authorizeRequests()
//                其他请求必须认证才能访问
                .anyRequest().authenticated();
    }
}

3. Implement your own UserDetails interface,

/**
 * 登录认证对象
 */
@Data
public class SignInIdentity implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private String roles;
    private Boolean isValid;
    // 角色集合
    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(StrUtil.isNotBlank(this.roles)) {
            // 获取数据库中的角色信息
            this.authorities = Stream.of(this.roles.split(",")).map(role -> new SimpleGrantedAuthority(role))
                    .collect(Collectors.toList());
        }else{
//            如果角色为空则设置为ROLE_USER
            this.authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
        }

        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.isValid;
    }
}

4. Implement your own UserDetailsService interface

        This interface provides a loadUserByUsername method. We generally obtain our user information by extending this interface, and the return value is the UserDetails defined in the previous step.

@Service
public class UserService implements UserDetailsService {
    @Resource
    private DinersMapper dinersMapper;
    @Resource
    private PasswordEncoder passwordEncoder;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        AssertUtil.isNotEmpty(username,"请输入用户名");
        Diners user = dinersMapper.selectByAccountInfo(username);
        if(user == null){
            throw new UsernameNotFoundException("用户名或密码错误,请重新输入");
        }
        // 初始化认证登录对象
        SignInIdentity signInIdentity = new SignInIdentity();
        BeanUtils.copyProperties(user, signInIdentity);
        signInIdentity.setPassword(passwordEncoder.encode(user.getPassword()));
        return signInIdentity;
    }
}

5. Create an authentication server configuration class

# Oauth2
client:
  oauth2:
    client-id: appId #客户端标识ID
    secret: 123456 #客户端安全码
#    授权类型
    grant-types:
      - password
      - refresh_token
    refresh-token-validity-time: 2000
#    token有效时间
    token-validity-time: 3600
#    客户端访问范围
    scopes:
      - api
      - all
/**
 * 授权服务
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Resource
    private ClientOauth2DataConfiguration clientOauth2DataConfiguration;
    @Resource
    private PasswordEncoder passwordEncoder;
//    认证管理对象
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private RedisTokenStore redisTokenStore;
    // 登录校验
    @Resource
    private UserService userService;
    /**
     * 配置令牌端点安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//        允许访问token的公钥,默认/oauth/token_key 是受保护的
        security.tokenKeyAccess("permitAll()")
//                允许检查token状态,默认/oauth/check_token 是受保护的
                .checkTokenAccess("permitAll()");
    }

    /**
     * 客户端配置 - 授权模型
     * 配置被允许访问这个认证服务器的客户端信息
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient(clientOauth2DataConfiguration.getClientId()) // 客户端标识ID
                .secret(passwordEncoder.encode(clientOauth2DataConfiguration.getSecret())) // 客户端安全码
                .authorizedGrantTypes(clientOauth2DataConfiguration.getGrantTypes()) // 授权类型
                .accessTokenValiditySeconds(clientOauth2DataConfiguration.getTokenValidityTime()) // token有效期
                .refreshTokenValiditySeconds(clientOauth2DataConfiguration.getRefreshTokenValidityTime()) //刷新token的有效期
                .scopes(clientOauth2DataConfiguration.getScopes()); // 客户端访问范围
    }

    /**
     * 配置授权以及令牌的访问端点和令牌服务
     * 授权服务器端点配置器
     * @param endpoints
     * @throws Exception
     */

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 认证器
        // password需要配置authenticationManager
        endpoints.authenticationManager(authenticationManager)
                // 具体的登录方法
                .userDetailsService(userService)
                // token存储方式
                .tokenStore(redisTokenStore)
                // 令牌增强对象,增强返回的结果
                .tokenEnhancer((accessToken, authentication) -> {
                    SignInIdentity signInIdentity = (SignInIdentity) authentication.getPrincipal();
                    LinkedHashMap<String, Object> map = new LinkedHashMap<>();
//                    追加额外信息
                    map.put("username",signInIdentity.getUsername());
                    map.put("authorities", signInIdentity.getAuthorities());
                    DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
                    token.setAdditionalInformation(map);
                    return token;
                 });
    }
}

        Among them, Authentication indicates the current authentication situation, and UserDetails (user information), Credentials (password), isAuthenticated (whether it has been authenticated), and Principal (user) can be obtained. If the Principal is authenticated, return UserDetails, if not authenticated, it is the username.

6. Rewrite the login method

/**
 * Oauth2控制器
 */
@RestController
@RequestMapping("/oauth")
public class OAuthController {
    @Resource
    private TokenEndpoint tokenEndpoint;
    @Resource
    private HttpServletRequest request;
    @PostMapping("/token")
    public ResultInfo postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        return custom(tokenEndpoint.postAccessToken(principal, parameters).getBody());
    }
    private ResultInfo custom(OAuth2AccessToken accessToken){
        DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
        Map<String, Object> data = new LinkedHashMap(token.getAdditionalInformation());
        data.put("accessToken",token.getValue());
        data.put("expireIn", token.getExpiresIn());
        data.put("scopes", token.getScope());
        if(token.getRefreshToken() != null) {
            data.put("refreshToken", token.getRefreshToken().getValue());
        }
        return ResultInfoUtil.buildSuccess(request.getServletPath(), data);
    }
}

Guess you like

Origin blog.csdn.net/qq_33235279/article/details/131724104