首先开下starter的结构:
|
|——-oauth2
|
|---auth-center-provider 认证中心
|
|---auth-provider-api 共用
|
|---auth-spring-boot-autoconifg auto
|
|---auth-spring-boot-starter starter
认证中心这里不说,后面有时间详细的进行讲解。
api模块,
Pom文件配置
授权中心API结构图
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth
spring-security-oauth2
2.2.1.RELEASE
com.codeus
saas-user
${project.version}
compile
org.springframework.security
spring-security-jwt
1.0.9.RELEASE
BaseUserDetail
/**
* 包装org.springframework.security.core.userdetails.User类
* @author 大仙
*
*/
public class BaseUserDetail implements UserDetails, CredentialsContainer {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* 用户
*/
private final BaseUser baseUser;
private final User user;
public BaseUserDetail(BaseUser baseUser, User user) {
this.baseUser = baseUser;
this.user = user;
}
@Override
public void eraseCredentials() {
user.eraseCredentials();
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return user.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return user.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return user.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
public BaseUser getBaseUser() {
return baseUser;
}
}
TokenEntity
/**
* token存储实体
* @author 大仙
*/
@Data
public class TokenEntity implements Serializable {
private String token;
private LocalDateTime invalidDate;
}
异常配置:
/**
* 权限相关异常
*/
public class AuthException extends SasException {
protected Integer code;
protected String title = "WeCode SAS Exception";
public AuthException(String message) {
super(message);
}
public AuthException(String message, int code) {
super(message);
this.code = code;
}
public AuthException(String message, int code, String title) {
super(message);
this.code = code;
this.title = title;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
JwtAccessToken
public class JwtAccessToken extends JwtAccessTokenConverter{
/**
* 生成token
* @param accessToken
* @param authentication
* @return
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
// 设置额外用户信息
if(authentication.getPrincipal() instanceof BaseUserDetail) {
BaseUser baseUser = ((BaseUserDetail) authentication.getPrincipal()).getBaseUser();
baseUser.setPassword(null);
// 将用户信息添加到token额外信息中
defaultOAuth2AccessToken.getAdditionalInformation().put(Constant.USER_INFO, baseUser);
}
return super.enhance(defaultOAuth2AccessToken, authentication);
}
/**
* 解析token
* @param value
* @param map
* @return
*/
@Override
public OAuth2AccessToken extractAccessToken(String value, Map map){
OAuth2AccessToken oauth2AccessToken = super.extractAccessToken(value, map);
convertData(oauth2AccessToken, oauth2AccessToken.getAdditionalInformation());
return oauth2AccessToken;
}
private void convertData(OAuth2AccessToken accessToken, Map map) {
accessToken.getAdditionalInformation().put(Constant.USER_INFO,convertUserData(map.get(Constant.USER_INFO)));
}
private BaseUser convertUserData(Object map) {
String json = JsonUtils.deserializer(map);
BaseUser user = JsonUtils.serializable(json, BaseUser.class);
return user;
}
}
util
public class JsonUtils {
private static ObjectMapper mapper = new ObjectMapper();
public JsonUtils() {
}
public static T serializable(String json, Class clazz) {
if (StringUtils.isEmpty(json)) {
return null;
} else {
try {
return mapper.readValue(json, clazz);
} catch (IOException var3) {
return null;
}
}
}
public static T serializable(String json, TypeReference reference) {
if (StringUtils.isEmpty(json)) {
return null;
} else {
try {
return mapper.readValue(json, reference);
} catch (IOException var3) {
return null;
}
}
}
public static String deserializer(Object json) {
if (json == null) {
return null;
} else {
try {
return mapper.writeValueAsString(json);
} catch (JsonProcessingException var2) {
return null;
}
}
}
static {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
}
TokenUtil
/**
* token控制工具类
* @author 大仙
*/
public class TokenUtil {
/**
* 存储token
* @param telephone
* @param redisTemplate
* @param token
* @return
*/
public static Boolean pushToken(String telephone, RedisTemplate redisTemplate, String token, Date invalid){
LocalDateTime invalidDate = invalid.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
long size = redisTemplate.opsForList().size(telephone);
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setInvalidDate(invalidDate);
tokenEntity.setToken(token);
if(size<=0){
redisTemplate.opsForList().rightPush(telephone,tokenEntity);
}else{
List tokenEntities = redisTemplate.opsForList().range(telephone, 0, size);
tokenEntities = tokenEntities.stream().filter(te -> te.getInvalidDate().isAfter(LocalDateTime.now())).collect(Collectors.toList());
if(tokenEntities.size()>= Constant.MAX_LOGIN){
return false;
}
tokenEntities.add(tokenEntity);
redisTemplate.delete(telephone);
tokenEntities.forEach(te->{
redisTemplate.opsForList().rightPush(telephone,te);
});
}
return true;
}
/**
* 判断token是否有效
* @param telephone
* @param redisTemplate
* @param token
* @return true 有效 false: 无效
*/
public static Boolean judgeTokenValid(String telephone, RedisTemplate redisTemplate, String token){
long size = redisTemplate.opsForList().size(telephone);
if(size<=0){
return false;
}else{
List tokenEntities = redisTemplate.opsForList().range(telephone, 0, size);
tokenEntities = tokenEntities.stream().filter(te->te.getToken().equals(token)).collect(Collectors.toList());
if(CollectionUtils.isEmpty(tokenEntities)){
return false;
}
TokenEntity tokenEntity = tokenEntities.get(0);
if(tokenEntity.getInvalidDate().isAfter(LocalDateTime.now())){
return true;
}
}
return false;
}
/**
* 登出
* @param telephone
* @param redisTemplate
* @param token
*/
public static void logout(String telephone, RedisTemplate redisTemplate, String token){
long size = redisTemplate.opsForList().size(telephone);
if(size<=0){
redisTemplate.delete(telephone);
}else{
List tokenEntities = redisTemplate.opsForList().range(telephone, 0, size);
tokenEntities = tokenEntities.stream().filter(te->!te.getToken().equals(token)).collect(Collectors.toList());
if(CollectionUtils.isEmpty(tokenEntities)){
redisTemplate.delete(telephone);
}
redisTemplate.delete(telephone);
tokenEntities.forEach(te->{
redisTemplate.opsForList().rightPush(telephone,te);
});
}
}
}
autoconfig模块:
/**
* 权限管理
*
* @author 大仙
*/
@ConfigurationProperties(prefix = "security")
public class AccessDecisionManagerIml implements AccessDecisionManager {
private Logger logger = LoggerFactory.getLogger(AccessDecisionManagerIml.class);
@Autowired
private AccessTokenUtils accessTokenUtils;
private AntPathMatcher matcher = new AntPathMatcher();
private String[] ignoreds;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void decide(Authentication authentication, Object o, Collection collection) throws AccessDeniedException, InsufficientAuthenticationException {
// 请求路径
String url = getUrl(o);
logger.info("请求URL:" + url);
// http 方法
String httpMethod = getMethod(o);
logger.info("请求类型:" + httpMethod);
//options的方法全部放行
if (httpMethod.equals(HttpMethod.OPTIONS.name())) {
return;
}
logger.info("白名单:"+Arrays.toString(ignoreds));
// 不拦截的请求
for (String path : ignoreds) {
String temp = path.trim();
if (matcher.match(temp, url)) {
return;
}
}
if(!TokenUtil.judgeTokenValid(accessTokenUtils.getUserInfo().getTelephone(),redisTemplate,accessTokenUtils.getAccessToken().getValue())){
throw new AccessDeniedException("无权限!");
}
// URL 鉴权
Iterator iterator = accessTokenUtils.getRoleInfo().iterator();
logger.info("url:"+url);
if(iterator!=null){
return;
}
logger.error("请求失败:url={},method={}",url,httpMethod);
throw new AccessDeniedException("无权限!");
}
/**
* 获取请求中的url
*/
private String getUrl(Object o) {
//获取当前访问url
String url = ((FilterInvocation) o).getRequestUrl();
int firstQuestionMarkIndex = url.indexOf("?");
if (firstQuestionMarkIndex != -1) {
return url.substring(0, firstQuestionMarkIndex);
}
return url.trim();
}
private String getMethod(Object o) {
return ((FilterInvocation) o).getRequest().getMethod();
}
private boolean matchUrl(String url, String modulePath) {
List urls = Arrays.asList(url.split("/")).stream().filter(e -> !"".equals(e)).collect(Collectors.toList());
Collections.reverse(urls);
List paths = Arrays.asList(modulePath.split("/")).stream().filter(e -> !"".equals(e)).collect(Collectors.toList());
Collections.reverse(paths);
// 如果数量不相等
if (urls.size() != paths.size()) {
return false;
}
for (int i = 0; i < paths.size(); i++) {
// 如果是 PathVariable 则忽略
String item = (String) paths.get(i);
if (item.charAt(0) != '{' && item.charAt(item.length() - 1) != '}') {
// 如果有不等于的,则代表 URL 不匹配
if (!item.equals(urls.get(i))) {
return false;
}
}
}
return true;
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
public void setIgnored(String ignored) {
if (ignored != null && !"".equals(ignored)) {
this.ignoreds = ignored.split(",");
} else {
this.ignoreds = new String[]{};
}
}
}
/**
* token工具类
* @author 大仙
*/
public class AccessTokenUtils {
@Autowired
private TokenStore tokenStore;
@Autowired
private HttpServletRequest request;
@Autowired
private TokenExtractor tokenExtractor;
@Autowired
private RedisTemplate redisTemplate;
/**
* 从token获取用户信息
* @return
*/
public BaseUser getUserInfo(){
BaseUser baseUser = (BaseUser) getAccessToken().getAdditionalInformation().get(Constant.USER_INFO);
if(baseUser==null){
throw new AccessDeniedException("无效TOKEN");
}
return baseUser;
}
/**
* 获取TOEKN
* @return
* @throws AccessDeniedException
*/
public OAuth2AccessToken getAccessToken() throws AccessDeniedException {
OAuth2AccessToken token;
// 抽取token
Authentication a = tokenExtractor.extract(request);
try {
// 调用JwtAccessTokenConverter的extractAccessToken方法解析token
token = tokenStore.readAccessToken((String) a.getPrincipal());
} catch(Exception e) {
throw new AccessDeniedException("AccessToken Not Found.");
}
return token;
}
/**
* 获取用户角色信息
* @return
*/
public List getRoleInfo(){
String userId = String.valueOf(getUserInfo().getId());
long size = redisTemplate.opsForList().size(userId);
return redisTemplate.opsForList().range(userId, 0, size);
}
}
/**
* 资源服务配置
* @ EnableResourceServer 启用资源服务
* @ EnableWebSecurity 启用web安全
* @ EnableGlobalMethodSecurity 启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
* @author 大仙
*/
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String PUBLIC_KEY = "pubkey.txt";
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(resJwtAccessTokenConverter());
}
@Override
public void configure(HttpSecurity http) throws Exception {
//禁用csrf
http.csrf().disable()
.formLogin().permitAll()
.and().httpBasic();
// 解决不允许显示在iframe的问题
http.headers().frameOptions().sameOrigin();
//拦截所有请求,下面2种写法都行
http.authorizeRequests().antMatchers("/**").authenticated();
}
@Bean
public JwtAccessTokenConverter resJwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessToken();
String publicKey = getPubKey();
converter.setVerifierKey(publicKey);
//不设置这个会出现 Cannot convert access token to JSON
converter.setVerifier(new RsaVerifier(publicKey));
return converter;
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
private String getPubKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
throw new AuthException("查询公钥出错");
}
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
}
}
/**
* 配置
* @author 大仙
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// 基于token,所以不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http // 配置授权管理器
.authorizeRequests().accessDecisionManager(getAccessDecisionManager())
// 匹配全部请求鉴权认证
.and().authorizeRequests().anyRequest().authenticated()
// 由于使用的是JWT,我们这里不需要csrf
.and().csrf().disable();
}
@Bean
public AccessDecisionManager getAccessDecisionManager() {
return new AccessDecisionManagerIml();
}
@Bean
public TokenExtractor getTokenExtractor() {
return new BearerTokenExtractor();
}
}
/**
* redis配置
* @author 大仙
*
*/
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate tokenEntityRedisTemplate() {
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new RedisObjectSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
/**
* redis编码解码类
* @Author: 朱维
* @Date 17:38 2019/11/27
*/
public class RedisObjectSerializer implements RedisSerializer {
static final byte[] EMPTY_ARRAY = new byte[0];
private Converter
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.codeus.auth.config.ResourceServerConfig,\
com.codeus.auth.config.AccessDecisionManagerIml,\
com.codeus.auth.config.AccessTokenUtils,\
com.codeus.auth.config.WebSecurityConfig,\
com.codeus.auth.redis.RedisConfig
starter模块
spring.provider:
provides: auth-spring-boot-autoconfigure
应用服务引用即可:
com.codeus
auth-spring-boot-starter
${project.version}
无需其他的配置