2021SC@SDUSC
- 零. spring security鉴权流程简图
- 一. 用gradle/maven导入依赖(gradle)
- 二. 建立配置文件
- 整体配置文件
- Options请求拦截器
- 配置用户名登录拦截
- Config&Filter
- handler
- JwtUtil
- UserDetailService
- 配置Token拦截
- config
- Filter
- Token(鉴权信息容器)
- Provider
- 三. Controller层验证
- 一些注意事项
首先导入以下依赖
// spring security implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.5.5' // jjwt implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1' // json implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6'二. 建立配置文件 整体配置文件
首先建立全局的Spring security的配置文件
// 开启spring security
@EnableWebSecurity
// 开启pre/post注解拦截
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用session与csrf,这里打算采用token的方式所以不用session
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).disable();
// 禁用form表单登录
http.formLogin().disable();
// 配置允许跨域
http.cors();
// 允许不经验证通过的接口
http.authorizeRequests().antMatchers("/login", "/register").permitAll()
// 其余接口均需要验证
.anyRequest().authenticated();
// option请求拦截器,cors请求如果是application/json之类的会首先做一次Options请求
http.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
.apply(new UsernameLoginConfig<>()) // 应用用户名密码登录的config
.and()
.apply(new JwtLoginConfig<>()) //应用jwt登录的config
.and().logout().disable(); //禁用默认的退出
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
// 暴露请求头,这里的authentication头是jwt的规范之一
configuration.addExposedHeader("Authorization");
UrlbasedCorsConfigurationSource source = new UrlbasedCorsConfigurationSource();
source.registerCorsConfiguration("
@Bean
PasswordEncoder getPw() {
return new BCryptPasswordEncoder();
}
}
Options请求拦截器
配置Options请求拦截器OptionsRequestFilter
public class OptionsRequestFilter extends oncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getMethod().equals("OPTIONS")) {
// 允许通过的请求,这里仅允许get/post/head如果需要可以添加put/delete等等
response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
response.setHeader("Access-Control-Allow-Headers", response.getHeader("Access-Control-Request-Headers"));
return;
}
filterChain.doFilter(request, response);
}
}
配置用户名登录拦截
Config&Filterspring security实际上有自己自带的UsernamePasswordAuthenticationFilter,我们在这儿就直接采用他的
首先是配置UsernameLoginConfig.java配置文件
@Configuration public class UsernameLoginConfighandler, B extends HttpSecurityBuilder> extends AbstractHttpConfigurer { // 使用spring security自带的用户名密码校验 // 默认的拦截/login的post请求,取出username password做校验 private final UsernamePasswordAuthenticationFilter authFilter = new UsernamePasswordAuthenticationFilter(); @Override public void configure(B builder) { authFilter.setPostOnly(true); // 获取系统默认的manager authFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class)); // 不使用session authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()); // 利用bean注入handler,可以交给spring控制生命周期,不用多次实例化 authFilter.setAuthenticationFailureHandler(loginFailureHandler()); authFilter.setAuthenticationSuccessHandler(usernameLoginSuccessHandler()); UsernamePasswordAuthenticationFilter filter = postProcess(authFilter); //指定Filter的位置 builder.addFilterAfter(filter, LogoutFilter.class); } @Bean LoginFailureHandler loginFailureHandler(){ return new LoginFailureHandler(); } @Bean UsernameLoginSuccessHandler usernameLoginSuccessHandler(){ return new UsernameLoginSuccessHandler(); } }
接着是配置UsernameLoginSuccessHandler,这个是用来处理登录成功后的流程,我们要做的是JWT的鉴权,因此在这个handler里应当生成jwt并给前端返回
public class UsernameLoginSuccessHandler implements AuthenticationSuccessHandler {
private final static Gson gson = new Gson();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException{
//生成token,并放置在header里
String token = JwtUtils.createToken(((BallUser)authentication.getPrincipal()).getUserId());
Map.Entry entry = JwtUtils.tokenToEntry(token);
response.setHeader(entry.getKey(), entry.getValue());
response.setCharacterEncoding("UTF-8");
response.getWriter().print(gson.toJson(ResultEntity.data(token)));
}
}
JwtUtil
这里放出来一个略微封装的jwt生成工具,可以根据项目自行定制
public class JwtUtils {
public static final String TOKEN_HEADER = "Authorization"; //Header标识JWT
private static final String TOKEN_PREFIX = "Bearer "; //JWT标准开头,注意空格
private static final String SECRET = "jwt-key"; //JWT签证密钥
private static final String ROLE = "role"; //Jwt中携带的身份key
private static final String USER_ID = "uid"; //Jwt中携带的用户ID的key
private static final long EXPIRATION = 60 * 60 * 24 * 7L; //过期时间7天
public static String tokenFromHeaders(Map headers) {
if (headers == null) {
return null;
}
for (Map.Entry entry : headers.entrySet()) {
if (entry.getKey().equals(TOKEN_HEADER)) {
if (entry.getValue().startsWith(TOKEN_PREFIX)) {
return entry.getValue().replaceFirst(TOKEN_PREFIX, "");
}
}
}
return null;
}
public static String tokenFromHeader(String header) {
if (header == null) {
return null;
}
if (header.startsWith(TOKEN_PREFIX)) {
return header.replaceFirst(TOKEN_PREFIX, "");
}
return null;
}
public static Map.Entry tokenToEntry(String token) {
assert token != null;
return new AbstractMap.SimpleEntry(TOKEN_HEADER, TOKEN_PREFIX + token);
}
public static String createToken(Integer userId) {
Map map = new HashMap<>();
map.put(USER_ID, userId);
return Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
.setSubject("info")
.setIssuer("weiteli")
.setAudience("together-ball")
.setClaims(map).setIssuedAt(new Date())
// token 7天内有效
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.compact();
}
public static String refreshToken(String token) {
Claims claim = getTokenClaims(token);
return Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
.setClaims(claim).setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.compact();
}
public static String getUsername(String token) {
try {
return getTokenClaims(token).getSubject();
} catch (Exception e) {
return null;
}
}
private static Claims getTokenClaims(String token) {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
e.printStackTrace();
}
return claims;
}
public static String getUserRole(String token) {
return (String) getTokenClaims(token).get(ROLE);
}
public static int getUserId(String token) {
return getTokenClaims(token).get(USER_ID, Integer.class);
}
public static Boolean isExpiration(String token) {
return getTokenClaims(token).getExpiration().before(new Date());
}
public static Date getIssuedAt(String token) {
return getTokenClaims(token).getIssuedAt();
}
}
接着是配置失败的统一处理,这个也很简单,默认是失败返回403,这里根据需求对其定制
ResultEntity以及StatusCode均是自定的类,输出string也是一样的
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
Gson gson = new Gson();
response.setCharacterEncoding("UTF-8");
response.getWriter().print(gson.toJson(ResultEntity.error(StatusCode.USER_CREDENTIALS_ERROR)));
response.setStatus(HttpStatus.OK.value());
}
}
UserDetailService
UsernamePasswordAuthenticationFilter会调用UserDetailService的loadUsername方法,一般需要在系统中重写service
首先定义鉴权的类,这个系统中通过userId唯一区分用户,因此需要重写
如果通过用户名唯一区分,直接使用org.springframework.security.core.userdetails.User可以不用重写
public class BallUser extends org.springframework.security.core.userdetails.User {
private final int userId;
public BallUser(int userId, String username, String password, Collection extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.userId = userId;
}
public BallUser(int userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.userId = userId;
}
public int getUserId() {
return userId;
}
}
接着重写service
@Service
public class UserService implements UserDetailsService {
@Autowired
UserDao userDao;
@Autowired
RoleDao roleDao;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.selectUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
List roles = roleDao.selectRolesByUserId(user.getUserId());
roles.add(RoleEnum.COMMON.getRoleName());
for (int i = 0; i < roles.size(); i++) {
roles.set(i, "ROLE_" + roles.get(i));
}
String roleString = StringUtils.arrayToCommaDelimitedString(roles.toArray());
return new BallUser(
user.getUserId(),
user.getUsername(),
user.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList(roleString));
}
public Boolean register(String username, String password) {
if (userDao.selectUserByUsername(username) == null) {
return userDao.insertUser(username, passwordEncoder.encode(password));
}
return false;
}
public List loadRolesByUserId(int userId){
List roles = roleDao.selectRolesByUserId(userId);
roles.add(RoleEnum.COMMON.getRoleName());
for (int i = 0; i < roles.size(); i++) {
roles.set(i, "ROLE_" + roles.get(i));
}
return roles;
}
}
配置Token拦截
config
首先还是配置config
@Configuration public class JwtLoginConfigFilter, B extends HttpSecurityBuilder> extends AbstractHttpConfigurer { @Override public void configure(B builder) throws Exception { builder.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } @Bean JwtAuthenticationFilter jwtAuthenticationFilter() { // 使用自定义的filter JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); // 新建providerManager并为filter配置provider ProviderManager providerManager = new ProviderManager(new JwtAuthenticationProvider()); filter.setAuthenticationManager(providerManager); // 不使用session filter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()); // 通过bean注入handler filter.setAuthenticationSuccessHandler(jwtLoginSuccessHandler()); filter.setAuthenticationFailureHandler(new LoginFailureHandler()); return filter; } @Bean JwtLoginSuccessHandler jwtLoginSuccessHandler(){ return new JwtLoginSuccessHandler(); } }
接着自定义filter,这里的jwt拦截需要我们自定义拦截方式
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter() {
// 拦截请求头中带有Authentication的
super(new RequestHeaderRequestMatcher(JwtUtils.TOKEN_HEADER));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/html;charset=UTF-8");
String token = getJwtToken(request);
JwtAuthenticationToken authRequest = new JwtAuthenticationToken(null, token, null);
return getAuthenticationManager().authenticate(authRequest);
}
// 这里一定要注意,必须重写,否则controller收不到请求
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
chain.doFilter(request, response);
}
private String getJwtToken(HttpServletRequest request) {
String header = request.getHeader(JwtUtils.TOKEN_HEADER);
return JwtUtils.tokenFromHeader(header);
}
}
Token(鉴权信息容器)
自定义Token用以保存鉴权的信息
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
// 保持与UsernameAuthentication的一致性
private final BallUser principal;
private final Object credentials;
public JwtAuthenticationToken(BallUser user, String token, Collection extends GrantedAuthority> authorities) {
super(authorities);
this.principal = user;
this.credentials = token;
}
@Override
public BallUser getPrincipal() {
return principal;
}
@Override
public Object getCredentials() {
return credentials;
}
}
Provider
接着自定义验证的Provider
// @Component 这个component也是不需要的
public class JwtAuthenticationProvider implements AuthenticationProvider {
// 这里注意,由于前面的实例都是new出来的,没有走spring的bean管理,这儿是没办法直接注入的
// 需要获取bean
// @Autowired
// RoleDao roleDao;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication;
String token = (String) jwtToken.getCredentials();
try {
int userId = JwtUtils.getUserId(token);
// 获取service bean
UserService userService = GetBeanUtils.getBean(UserService.class);
List roles = userService.loadRolesByUserId(userId);
BallUser user = new BallUser(
userId, "[NotNeed]", "[Protected]",
AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(roles.toArray())));
return new JwtAuthenticationToken(user, token, user.getAuthorities());
} catch (JwtException e) {
e.printStackTrace();
throw new AuthenticationServiceException("Token invalidate");
}
}
@Override
public boolean supports(Class> authentication) {
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
}
}
Spring Security是通过鉴权过程中的异常来判断是否鉴权成功的,如果中途任何地方抛出AuthenticationException异常,即认为鉴权失败,没有任何异常返回结果认为鉴权成功
附录:
GetBeanUtils的内容参见 这篇博客
GetBeanUtils内容如下
// [这篇博客](https://blog.csdn.net/qq_28080659/article/details/99687074)
@Component
public class GetBeanUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (GetBeanUtils.applicationContext == null) {
GetBeanUtils.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}
public static T getBean(Class c) {
return applicationContext.getBean(c);
}
public static T getBean(String name, Class c) {
return getApplicationContext().getBean(name, c);
}
}
三. Controller层验证
controller代码如下
@RestController
@RequestMapping("/")
public class UserController {
@Autowired
UserService userService;
@RequestMapping("register")
ResultEntity register(@RequestParam String username, @RequestParam String password) {
if (userService.register(username, password)) {
return ResultEntity.success();
} else {
return ResultEntity.error(StatusCode.USER_ALREADY_EXISTING);
}
}
@RequestMapping("logout")
@PreAuthorize("hasRole('COMMON')")
// @PreAuthorize("hasRole('ROLE_COMMON')") 这么写与上一行等价,ROLE_可省
ResultEntity logout() {
return ResultEntity.success();
}
}
-
不需要权限的接口 /register直接访问
-
需要权限的接口 /logout需要权限
在有权限的情况下
略微修改上面的controller代码,再次请求接口
@RequestMapping("logout") @PreAuthorize("hasRole('SYSTEM')") ResultEntity logout() { return ResultEntity.success(); }这里会显示没有权限返回403
-
隐含的拦截器接口 /login
- 用到autowired的类,除了必须要有@Component/@Service等注解意外,必须要保证,其声明周期由springboot管理,而不是直接new出来的
- jwt要求的规范一定要注意,header是Authoritarian内容是bearer紧跟token,特别注意bearer后面有个space空格
- 重写AbstractAuthenticationProcessingFilter的时候,必须重写successfulAuthentication方法,否则后面的请求链会收不到请求,重写的时候需要添加chain.doFilter(request, response);
- 如果在new出来的类中想要使用spring的bean,可以参照配置token拦截的最后一部分使用GetBeanUtil



