一、OAuth2认证模式
一共四种认证方式,授权码模式、密码模式、简化模式和客户端模式。实现单点登录,比较流行的方法是使用jwt方式,jwt是无状态的,qit本身就能携带信息,因此服务端可以不用保存他的信息,但只要token不过期,用户就可以一直访问,这样就无法实现退出功能。如果要实现退出登录功能,就需要在服务端存储token信息,这就违背了使用jwt认证的初衷。
二、创建Security配置类
在这个类中注入需要用到的工具类以及配置放行和认证规则
/** * SecurityConfiguration 配置类 */@Configuration@EnableWebSecuritypublic class SecurityConfiguration extends WebSecurityConfigurerAdapter {//注入Redis连接工厂@Resourceprivate RedisConnectionFactory redisConnectionFactory;//Redis仓库类,初始化RedisTokenStore用于将Token存储到Redis@Beanpublic RedisTokenStore redisTokenStore(){RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);redisTokenStore.setPrefix("TOKEN:"); //设置KEY的层级前缀,方便查询return redisTokenStore;}//初始化密码编辑器,用MD5加密密码@Beanpublic PasswordEncoder passwordEncoder(){return new PasswordEncoder() {/** * 加密 * @param rawPassword 原始密码 * @return */@Overridepublic String encode(CharSequence rawPassword) {return DigestUtil.md5Hex(rawPassword.toString());}/** * 校验密码 * @param rawPassword 原始密码 * @param encodedPassword 加密密码 * @return */@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return DigestUtil.md5Hex(rawPassword.toString()).equals(encodedPassword);}};}//初始化认证管理对象,密码登录方式需要用到@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//放行和认证规则@Overrideprotected void configure(HttpSecurity http) throws Exception {// csrf认为除了get以外的请求都是不安全的,禁用http.csrf().disable().authorizeRequests()//放行的请求.antMatchers("/oauth/**", "/actuator/**").permitAll().and().authorizeRequests()//其他请求必须认证才能访问.anyRequest().authenticated();}}
三、实现自己的UserDetails接口,
/** * 登录认证对象 */@Datapublic class SignInIdentity implements UserDetails {private Integer id;private String username;private String password;private String roles;private Boolean isValid;// 角色集合private List authorities;@Overridepublic Collection getAuthorities() {if(StrUtil.isNotBlank(this.roles)) {// 获取数据库中的角色信息this.authorities = Stream.of(this.roles.split(",")).map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList());}else{//如果角色为空则设置为ROLE_USERthis.authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");}return this.authorities;}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return this.isValid;}}
四、实现自己的UserDetailsService接口
该接口提供一个loadUserByUsername方法,我们一般通过扩展这个借口来获取我们的用户信息,返回值就是上一步中定义的UserDetails。
@Servicepublic class UserService implements UserDetailsService {@Resourceprivate DinersMapper dinersMapper;@Resourceprivate PasswordEncoder passwordEncoder;@Overridepublic 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;}}
五、创建认证服务器配置类
# Oauth2client:oauth2:client-id: appId #客户端标识IDsecret: 123456 #客户端安全码#授权类型grant-types:- password- refresh_tokenrefresh-token-validity-time: 2000#token有效时间token-validity-time: 3600#客户端访问范围scopes:- api- all
/** * 授权服务 */@Configuration@EnableAuthorizationServerpublic class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {@Resourceprivate ClientOauth2DataConfiguration clientOauth2DataConfiguration;@Resourceprivate PasswordEncoder passwordEncoder;//认证管理对象@Resourceprivate AuthenticationManager authenticationManager;@Resourceprivate RedisTokenStore redisTokenStore;// 登录校验@Resourceprivate UserService userService;/** * 配置令牌端点安全约束 * @param security * @throws Exception */@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {//允许访问token的公钥,默认/oauth/token_key 是受保护的security.tokenKeyAccess("permitAll()")//允许检查token状态,默认/oauth/check_token 是受保护的.checkTokenAccess("permitAll()");}/** * 客户端配置 - 授权模型 * 配置被允许访问这个认证服务器的客户端信息 * @param clients * @throws Exception */@Overridepublic 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 */@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {// 认证器// password需要配置authenticationManagerendpoints.authenticationManager(authenticationManager)// 具体的登录方法.userDetailsService(userService)// token存储方式.tokenStore(redisTokenStore)// 令牌增强对象,增强返回的结果.tokenEnhancer((accessToken, authentication) -> {SignInIdentity signInIdentity = (SignInIdentity) authentication.getPrincipal();LinkedHashMap map = new LinkedHashMap();//追加额外信息map.put("username",signInIdentity.getUsername());map.put("authorities", signInIdentity.getAuthorities());DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;token.setAdditionalInformation(map);return token; });}}
其中Authentication表示当前的认证情况,可以获取UserDetails(用户信息),Credentials(密码),isAuthenticated(是否已经认证过),Principal(用户)。Principal如果认证了,返回UserDetails,如果没有认证,就是用户名。
六、重写登录方法
/** * Oauth2控制器 */@RestController@RequestMapping("/oauth")public class OAuthController {@Resourceprivate TokenEndpoint tokenEndpoint;@Resourceprivate HttpServletRequest request;@PostMapping("/token")public ResultInfo postAccessToken(Principal principal, @RequestParam Map parameters) throws HttpRequestMethodNotSupportedException {return custom(tokenEndpoint.postAccessToken(principal, parameters).getBody());}private ResultInfo custom(OAuth2AccessToken accessToken){DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;Map 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);}}