前言
认证授权功能几乎是每个系统必备功能,本文实现基于Spring Security安全认证开发Maven组件,方便以后项目添加依赖即可使用。
创建Maven项目
环境
- Spring Boot:2.1.17
- Spring Cloud:Finchley.RELEASE
pom.xml文件
<groupId>com.fang</groupId>
<artifactId>auth-server</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<lombok.version>1.16.20</lombok.version>
<fastjson.version>1.2.74</fastjson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
实现认证功能
1、登录成功处理,登录成功后保存SecurityContext到Redis。
@Component
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = UUID.randomUUID().toString();
saveTokenToRedis(authentication.getPrincipal().toString(), token);
String ipAddress = IpUtil.getIpAddress(request);
String userAgent = BrowserUtil.getUserAgent(request);
// 客户端IP和UserAgent拼接后AES加密,防止cookie被窃取
String encryptToken = AesUtil.encrypt(token, ipAddress + userAgent);
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
Map<String, Object> result = new HashMap<>(3);
result.put("code", 200);
result.put("message", "success");
result.put("data", encryptToken);
response.getWriter().print(JSON.toJSONString(result));
}
private void saveTokenToRedis(String username, String token) {
String oldToken = (String) redisTemplate.opsForValue().get(username);
if (!StringUtils.isEmpty(oldToken)) {
redisTemplate.opsForHash().delete(oldToken, Constants.KEY_CONTEXT);
}
redisTemplate.opsForValue().set(username, token);
redisTemplate.opsForHash().put(token, Constants.KEY_CONTEXT, SecurityContextHolder.getContext());
redisTemplate.expire(token, Constants.EXPIRE_TOKEN, TimeUnit.SECONDS);
}
}
2、登录失败处理,直接返回异常信息。
@Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
Map<String, Object> result = new HashMap<>(2);
result.put("code", 405);
result.put("message", e.getMessage());
response.getWriter().print(JSON.toJSONString(result));
}
}
3、退出登录处理,清理Redis。
@Component
public class RestLogoutSuccessHandler implements LogoutSuccessHandler {
private static final Log logger = LogFactory.getLog(RestLogoutSuccessHandler.class);
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String token = RedisSecurityContextRepository.extractToken(request);
System.out.println("logout success: " + token);
if (!StringUtils.isEmpty(token)) {
CookieUtil.delCookie(request, response, Constants.HEADER_AUTHENTICATION);
redisTemplate.delete(token);
}
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
new SecurityContextLogoutHandler().logout(request, response, auth);
response.setHeader(Constants.HEADER_AUTHENTICATION, null);
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
Map<String, Object> result = new HashMap<>(2);
result.put("code", 200);
result.put("message", "success");
response.getWriter().print(JSON.toJSONString(result));
}
}
4、匿名用户访问权限资源处理,返回401。
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
Map<String, Object> result = new HashMap<>(2);
result.put("code", 401);
result.put("message", e.getMessage());
response.getWriter().print(JSON.toJSONString(result));
}
}
5、登录用户没有权限处理,返回403。
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpStatus.FORBIDDEN.value());
Map<String, Object> result = new HashMap<>(2);
result.put("code", 403);
result.put("message", e.getMessage());
response.getWriter().print(JSON.toJSONString(result));
}
}
实现权限控制功能
1、定义url权限配置文件。
@ConfigurationProperties(prefix = "auth.permission")
public class UrlRoleProperites {
public List<UrlRoleDefinition> getUrlRoles() {
return urlRoles;
}
public void setUrlRoles(List<UrlRoleDefinition> urlRoles) {
this.urlRoles = urlRoles;
}
private List<UrlRoleDefinition> urlRoles = new ArrayList<>();
public static class UrlRoleDefinition {
private String url;
private String[] roles;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String[] getRoles() {
return roles;
}
public void setRoles(String[] roles) {
this.roles = roles;
}
}
}
2、权限过滤,匹配需要权限的url,不需要权限的url直接返回null。
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private UrlRoleProperites urlRoleProperites;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Value("${auth.authenticated.patterns}")
private String[] permitPatterns;
/**
* 返回本次访问需要的权限,可以有多个权限。
* 如果没有匹配的url直接返回null,也就是没有配置权限的url默认都为白名单
* @param o
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) o).getRequestUrl();
for (UrlRoleProperites.UrlRoleDefinition item : urlRoleProperites.getUrlRoles()) {
if (antPathMatcher.match(item.getUrl(), requestUrl) && item.getRoles().length > 0) {
System.out.println("当前访问的路径:" + requestUrl + ",需要的权限:" + JSON.toJSONString(item.getRoles()));
return SecurityConfig.createList(item.getRoles());
}
}
for (String url : permitPatterns) {
if (antPathMatcher.match(url, requestUrl)) {
return SecurityConfig.createList("ROLE_LOGIN");
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 告知调用者当前SecurityMetadataSource是否支持此类安全对象,只有支持的时候,才能对这类安全对象调用getAttributes方法。
* @param aClass
* @return
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
- permitPatterns属性:定义需要认证的路径
3、权限匹配,ROLE_LOGIN
表示需要登录,能够到循环里表示已经认证过,直接返回即可。
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
System.out.println("JSON.toJSONString(authentication) = " + JSON.toJSONString(authentication));
if (authentication == null || authentication.getName().equals("anonymousUser")) {
throw new AccessDeniedException("没有访问权限");
}
Iterator<ConfigAttribute> iterator = collection.iterator();
while (iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
String role = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(role)) {
return;
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(role)) {
return;
}
}
}
throw new AccessDeniedException("权限不足");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
安全认证配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Autowired
private AccessDecisionManager accessDecisionManager;
@Autowired
private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${auth.authenticated.patterns}")
private String[] permitPatterns;
public CustomAuthenticationProvider getAuthenticationProvider() {
System.out.println("userDetailsService = " + userDetailsService);
CustomAuthenticationProvider authenticationProvider = new CustomAuthenticationProvider(stringRedisTemplate);
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
return authenticationProvider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 不能在CustomAuthenticationProvider添加@Component注解,需要通过new方法然后注入userDetailsService
auth.authenticationProvider(getAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用CSRF
http.csrf().disable().cors();
http.authorizeRequests()
// .antMatchers(permitPatterns).authenticated() // 和withObjectPostProcessor一起没效果
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
o.setAccessDecisionManager(accessDecisionManager);
return o;
}
}) //设置后置处理程序对象
.anyRequest().permitAll().and()
.formLogin().loginProcessingUrl("/oauth/token").permitAll()
.failureHandler(authenticationFailureHandler)
.successHandler(authenticationSuccessHandler).and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler).and()
.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
//没有登录异常
.authenticationEntryPoint(authenticationEntryPoint).and()
//禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
http.securityContext().securityContextRepository(getSecurityContextRepository());
}
SecurityContextRepository getSecurityContextRepository() {
return new RedisSecurityContextRepository(redisTemplate);
}
@Bean
public UrlRoleProperites urlRoleProperites() {
return new UrlRoleProperites();
}
}
- 不需要权限控制去掉
withObjectPostProcessor
就可以,加上.antMatchers(permitPatterns).authenticated()
。 - 为了实现
.antMatchers(permitPatterns).authenticated()
功能,通过在UrlFilterInvocationSecurityMetadataSource
实现路径权限判断。 RedisSecurityContextRepository
类、RedisConfig
类和一些工具类这里省略。
到这里就可以使用了。
实现多次登录失败暂停登录功能
1、需要自定义AuthenticationProvider,在登录前判断登录失败次数,认证失败更新登录失败次数。
public class CustomAuthenticationProvider extends DaoAuthenticationProvider {
protected final Log logger = LogFactory.getLog(this.getClass());
private StringRedisTemplate redisTemplate;
public CustomAuthenticationProvider(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
super.setPasswordEncoder(passwordEncoder);
}
@Override
public void setUserDetailsService(UserDetailsService userDetailsService) {
super.setUserDetailsService(userDetailsService);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
logger.info("auth user: " + authentication.getName());
// 登录前验证
attempAuthenticate(authentication.getName());
// 调用上层验证逻辑
Authentication auth = super.authenticate(authentication);
// 如果验证通过登录成功则重置尝试次数, 否则抛出异常
resetFailAttemps(authentication.getName());
return auth;
}
catch (BadCredentialsException e) {
// 如果验证不通过,则更新尝试次数
updateFailAttemps(authentication.getName());
throw e;
}
}
private void attempAuthenticate(String name) {
Serializable serializable = redisTemplate.opsForValue().get(name);
logger.info("get redis value: " + serializable);
if (serializable == null) {
return;
}
try {
int times = Integer.parseInt(serializable.toString());
if (times > Constants.MAX_LOGIN_TIMES) {
throw new LockedException("登录失败次数超过" + Constants.MAX_LOGIN_TIMES
+ "次,请" + Constants.EXPIRE_LOGIN_FAIL / 60 + "分钟后再试");
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
private void updateFailAttemps(String name) {
redisTemplate.opsForValue().setIfAbsent(name, "1", Constants.EXPIRE_LOGIN_FAIL, TimeUnit.SECONDS);
redisTemplate.opsForValue().increment(name);
}
private void resetFailAttemps(String name) {
redisTemplate.delete(name);
}
}
- 注意用到redis的increment功能,需要使用
StringRedisTemplate
。 - 不能在CustomAuthenticationProvider添加@Component注解,需要通过new方法然后注入userDetailsService。
使用
1、创建Spring Boot项目,添加依赖。
<dependency>
<groupId>com.fang</groupId>
<artifactId>auth-server</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2、修改配置文件
auth.authenticated.patterns=/auth/**
auth.permission.url-roles[0].url=/auth/test
auth.permission.url-roles[0].roles=ROLE_admin,ROLE_user
- auth.authenticated.patterns:定义哪些路径需要登录
- auth.permission.url-roles:定义路径和对应需要什么权限,路径可以是list,路径的权限是list。
3、根据自己的业务实现UserDetailsService
接口,正常是查询数据库用户信息。
@Slf4j
@Service
public class LoginService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.info("username: " + s);
Set<SimpleGrantedAuthority> simpleGrantedAuthoritySet = new HashSet<>();
if ("admin".equals(s)) {
simpleGrantedAuthoritySet.add(new SimpleGrantedAuthority("ROLE_admin"));
return new User("admin", "$2a$10$oEZnA0YTINrwxysn.e2HsuGIPSbtd9EJzj6n.6PvYpFJ.FlGdmNIO", simpleGrantedAuthoritySet);
}
simpleGrantedAuthoritySet.add(new SimpleGrantedAuthority("ROLE_user"));
return new User("111", "eowYHBL0OwfcyVQra4EY7gQW50f6CbXNnp8V2pBhCrtIffccZFlYAmQBzHDGbFmq", simpleGrantedAuthoritySet);
}
}
4、自定义密码加密算法,加上AES解密是为了前端发送密码时可以用AES加密,不通过明文传输。
@Component
public class CustomBCryptPasswordEncoder extends BCryptPasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
String decrypt = null;
try {
decrypt = AesUtil.decrypt(rawPassword.toString(), "fang");
} catch (Exception e) {
e.printStackTrace();
}
if (decrypt == null) {
return super.encode(rawPassword);
}
return super.encode(decrypt);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String decrypt = null;
try {
decrypt = AesUtil.decrypt(rawPassword.toString(), "fang");
} catch (Exception e) {
e.printStackTrace();
}
if (decrypt == null) {
return super.matches(rawPassword, encodedPassword);
}
return super.matches(decrypt, encodedPassword);
}
}
测试
1、访问http://localhost:8080/auth/str
,返回:
{
"message": "Full authentication is required to access this resource",
"code": 401
}
2、访问http://localhost:8080/user/str
,返回:
{
"code": 0,
"message": "成功",
"data": "anonymousUser"
}
3、登录http://localhost:8080/oauth/token
,POST方式,username: admin1,password:5Ov4vAoTgzCTAgUnAEo_AA==
返回:
{
"message": "用户名或密码错误",
"code": 405
}
登录5次后返回:
{
"message": "登录失败次数超过5次,请5分钟后再试",
"code": 405
}
4、登录http://localhost:8080/oauth/token
,POST方式,username: admin,password:5Ov4vAoTgzCTAgUnAEo_AA==
返回:
{
"message": "success",
"data": "NEcXUcPjPOwEJhb-u-oyCfd4OMx-8TP4uo1NAR62XK-6MtYmAXwhNxbPmktZeylk",
"code": 200
}
5、访问http://localhost:8080/auth/str
,设置Header:”Authorization”: “NEcXUcPjPOwEJhb-u-oyCfd4OMx-8TP4uo1NAR62XK-6MtYmAXwhNxbPmktZeylk”
返回:
{
"code": 0,
"message": "成功",
"data": "org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_admin"
}
6、访问http://localhost:8080/auth/test
,设置Header:”Authorization”: “NEcXUcPjPOwEJhb-u-oyCfd4OMx-8TP4uo1NAR62XK-6MtYmAXwhNxbPmktZeylk”,返回:
{
"code": 0,
"message": "成功",
"data": {
"data": "[{\"authority\":\"ROLE_admin\"}]"
}
}
7、修改配置文件:auth.permission.url-roles[0].roles=ROLE_user
,重启服务,访问http://localhost:8080/auth/test
,返回:
{
"message": "权限不足",
"code": 403
}
- 控制台打印:
当前访问的路径:/auth/test,需要的权限:["ROLE_user"]
总结
AbstractAuthenticationProcessingFilter
处理登录:先根据发送的参数封装成Token,再获取用户详细信息,然后认证是否通过。AbstractSecurityInterceptor
安全控制:处理请求前先获取安全资源所需权限,判断当前用户是否有权限访问资源。