Spring Security 初探 & 自定义身份认证
2018/7/22大约 6 分钟
一直以来写Web应用时对于身份的验证和授权都比较简单,借助session attribute,又或者使用自己实现的认证拦截器(filter、interceptor……)等。
但其实spring security有提供一整套根据不同标准制定的处理认证授权的方法,而且还支持自定义认证。这次要讲的是通过自定义认证来实现JWT,关于spring security的更详细讲解还需要准备一下待下篇。
DEMO 目录结构
目录如下
- src/main/java
- indi.a9043.demo
- config
- SecurityConfig.java
- controller
- TestController.java
- entity
- TestUser.java
- security
- TestAccessDeniedHandler.java
- TestAuthenticationEntryPoint.java
- TestAuthenticationFailureHandler.java
- TestAuthenticationFilter.java
- TestAuthenticationSuccessHandler.java
- service
- TestUserDetailsService.java
- util
- JwtUtil.java
- DemoApplication.java
- config
- indi.a9043.demo
如上表:
- config 为项目配置类,demo只有一个security配置
- controller 测试的接口
- entity 认证用的实体类
- security 自定义认证相关类
- service 业务类,demo包含身份验证的Service
- util 工具类,demo包含JWT使用工具类
- DemoApplication main函数入口
开始构建
本项目使用spring boot initializer预先添加,项目依赖和入口类,pom.xml 依赖如下,可以自行添加。
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20180130</version>
</dependency>
</dependencies>新建用户实体以及用户操作类
在entity包下创建TestUser 类并实现UserDetails接口
package indi.a9043.demo.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* 仅考虑简单认证
*/
public class TestUser implements UserDetails {
private String userName;
private String userPassword;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.userPassword;
}
@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 true;
}
//getter & setter
public String getUserName() {
return userName;
}
public TestUser setUserName(String userName) {
this.userName = userName;
return this;
}
public String getUserPassword() {
return userPassword;
}
public TestUser setUserPassword(String userPassword) {
this.userPassword = userPassword;
return this;
}
}在service包下新建TestUserDetailsService 并实现UserDetailsService
package indi.a9043.demo.service;
import indi.a9043.demo.entity.TestUser;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class TestUserDetailsService implements UserDetailsService {
/*
* 定义一个用户用来测试
* 用户名: test1
* 密码: 123456
*/
private TestUser testUser;
public TestUserDetailsService() {
testUser = new TestUser();
testUser.setUserName("test1");
testUser.setUserPassword(new BCryptPasswordEncoder().encode("123456"));
}
/**
* 根据用户名查询UserDetails
*
* @param username 用户名
* @return UserDetails
* @throws UsernameNotFoundException 无此用户名
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username.equals(testUser.getUserName())) {
return testUser;
}
throw new UsernameNotFoundException("User " + username + " was not found");
}
}新建JwtUtil 工具类
util包下
package indi.a9043.demo.util;
import io.jsonwebtoken.*;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
/**
* Token Util
*
* @author a9043
*/
public class JwtUtil {
private static SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512;
private static String stringKey = "a9043_xxx";
private static byte[] encodedKey = Base64.encodeBase64(stringKey.getBytes());
private static SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
/**
* 生成Token
*
* @param claims 声明
* @return token
*/
public static String createJWT(Map<String, Object> claims) {
return createJWT(claims, Calendar.HOUR, 2);
}
/**
* 自定义过期时间Token
*
* @param claims 声明
* @param expireField 时间位
* @param expireAmount 时间长
* @return token
*/
public static String createJWT(Map<String, Object> claims, int expireField, int expireAmount) {
Date now = Calendar.getInstance().getTime();
Calendar expireCal = Calendar.getInstance();
expireCal.add(expireField, expireAmount);
Date expire = expireCal.getTime();
JwtBuilder builder = Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setHeaderParam("alg", "HS512")
.setIssuedAt(now)
.setClaims(claims)
.setIssuer("a9043")
.setExpiration(expire)
.signWith(signatureAlgorithm, key);
return builder.compact();
}
/**
* Token parser
*
* @param JwtStr token
* @return claims
* @throws SignatureException err
* @throws ExpiredJwtException err
*/
public static Claims parseJwt(String JwtStr) throws SignatureException, ExpiredJwtException {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(JwtStr)
.getBody();
return Optional
.ofNullable(claims)
.filter(claim -> claim.getExpiration() == null || !new Date().after(claim.getExpiration()))
.orElse(null);
}
}新建测试接口
controller 包下
@RestController
public class TestController {
/**
* 不受保护资源
*
* @return
*/
@GetMapping("/hello")
public String hello() {
return "hello";
}
/**
* 受保护资源
*
* @return
*/
@GetMapping("/resource")
public String resource() {
return "resource";
}
}新建token过滤器
security包下新建 TestAuthenticationFilter 继承 OncePerRequestFilter
package indi.a9043.demo.security;
import indi.a9043.demo.service.TestUserDetailsService;
import indi.a9043.demo.util.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class TestAuthenticationFilter extends OncePerRequestFilter {
@Resource
private TestUserDetailsService testUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//获得header token
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = header.substring(7);
Claims claims;
if (token.length() <= 0 ||
SecurityContextHolder.getContext().getAuthentication() != null) {
return;
}
//解析token
try {
claims = JwtUtil.parseJwt(token);
String userName = (String) claims.get("userName");
UserDetails userDetails = testUserDetailsService.loadUserByUsername(userName);
if (!userDetails.getUsername().equals(userName)) {
SecurityContextHolder.clearContext();
filterChain.doFilter(request, response);
return;
}
//设定Authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails.getUsername(),
userDetails.getPassword(),
userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().
buildDetails(
request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (MalformedJwtException | SignatureException | ExpiredJwtException e) {
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}新建各处理类
都新建在security包下
- TestAuthenticationSuccessHandler 登录成功
package indi.a9043.demo.security;
import indi.a9043.demo.entity.TestUser;
import indi.a9043.demo.util.JwtUtil;
import org.json.JSONObject;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class TestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
TestUser testUser = (TestUser) authentication.getPrincipal();
Map<String, Object> claimsMap = new HashMap<>();
claimsMap.put("userName", testUser.getUserName());
String token = JwtUtil.createJWT(claimsMap);
JSONObject jsonObject = new JSONObject();
jsonObject.put("access-token", token);
response.getWriter().write(jsonObject.toString());
}
}- TestAuthenticationFailureHandler 登录失败
package indi.a9043.demo.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class TestAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.getWriter().write("failure");
}
}- TestAccessDeniedHandler 登录后的拒绝handler
package indi.a9043.demo.security;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class TestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.getWriter().write("denied");
}
}- TestAuthenticationEntryPoint 未认证入口
package indi.a9043.demo.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class TestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}配置 SecurityConfig
此为web security 配置类,不同版本和构建的spring可能需要带上 @EnableWebSecurity 注解
package indi.a9043.demo.config;
import indi.a9043.demo.security.*;
import indi.a9043.demo.service.TestUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private TestUserDetailsService testUserDetailsService;
@Resource
private TestAuthenticationEntryPoint testAuthenticationEntryPoint;
@Resource
private TestAuthenticationSuccessHandler testAuthenticationSuccessHandler;
@Resource
private TestAuthenticationFailureHandler testAuthenticationFailureHandler;
@Resource
private TestAccessDeniedHandler testAccessDeniedHandler;
@Resource
private DaoAuthenticationProvider daoAuthenticationProvider;
@Resource
private TestAuthenticationFilter testAuthenticationFilter;
@Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
/*
* 自定义Provider
* 自定义UserDetailsService
* 自定义PasswordEncoder
*/
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(testUserDetailsService);
daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
return daoAuthenticationProvider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*
* add 自定义provider
*/
auth
.authenticationProvider(daoAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 关闭csrf保护
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置为无状态
.and()
.authorizeRequests()
.antMatchers("/hello").permitAll() // 不受保护资源
.anyRequest().authenticated() //受保护资源
.and()
.formLogin() //表单登录
.loginProcessingUrl("/login") //表单登录api 不同于loginPage登录页面
.successHandler(testAuthenticationSuccessHandler) //登录成功handler
.failureHandler(testAuthenticationFailureHandler) //登录失败handler
.and()
.exceptionHandling()
.authenticationEntryPoint(testAuthenticationEntryPoint) //未授权入口
.accessDeniedHandler(testAccessDeniedHandler) //拒绝入口
.and()
.addFilterBefore(testAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class); //添加token过滤, 处理带token请求
}
}测试
综上,已经security 已经配置好了,现在开始测试 rest api
- 访问不受保护资源 /hello
req:
GET http://localhost:8080/hello
Accept: */*
Cache-Control: no-cacheres:
hello- 访问受保护资源 /resource
req:
GET http://localhost:8080/resource
Accept: */*
Cache-Control: no-cacheres:
{
"timestamp": "2018-07-22T13:53:31.134+0000",
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource",
"path": "/resource"
}- 登录接口 /login
req:
POST http://localhost:8080/login?username=test1&password=123456
Accept: */*
Cache-Control: no-cacheres:
{
"access-token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJhOTA0MyIsInVzZXJOYW1lIjoidGVzdDEiLCJleHAiOjE1MzIyNzQ0MjR9.sKfslwlYyn8lKZ0UQdNqKdBB2aheoufs5O2p3brUxxKyn5cAN_0hOqCB7LkmhdMVe4XIwpjAiez48Gqwqb8AIQ"
}- 访问受保护资源 /resource
req:
GET http://localhost:8080/resource
Accept: */*
Cache-Control: no-cache
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJhOTA0MyIsInVzZXJOYW1lIjoidGVzdDEiLCJleHAiOjE1MzIyNzQ0MjR9.sKfslwlYyn8lKZ0UQdNqKdBB2aheoufs5O2p3brUxxKyn5cAN_0hOqCB7LkmhdMVe4XIwpjAiez48Gqwqb8AIQres:
resource总结
没有总结。。自此,我总算尝试了通过Spring Security,使用自定义方式进行身份认证。
demo 源代码托管在github:https://github.com/190434957/spring_security_JWT_DEMO
