一个能够为基于Spring的企业应用系统提供声明式的安全訪问控制解决方式的安全框架(简单说是对访问权限进行控制嘛),应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。spring security的主要核心功能为 认证和授权,所有的架构也是基于这两个核心功能去实现的。
在spring boot项目中集成security 在pom文件中引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <!--security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-data</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <!--jwt--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
登录时认证 1 2 3 4 5 6 7 8 9 10 11 12 13 public Object login(SignInForm form) { Authentication authentication; // try { authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken (form.getUsername(), form.getPassword())); // } catch (Exception e) { // throw new CommonException(e.getMessage()); // } return tokenProvider.createToken(authentication, form.getRememberMe()); }
security使用Authentication对象来描述当前用户的信息, AuthenticationManager是security自带的一个类,使用@Autowired注解注入即可。
UsernamePasswordAuthenticationToken是security使用username和password生成的一个Authentication对象,拿到该对象后还要进行其它的操作如验证用户名密码是否正确,获取用户角色权限等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider { @Autowired private UserDetailsService securityUserDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { renull } @Override public boolean supports(Class<?> authentication) { return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); } }
可以用的扩展逻辑并不是直接用的继承UsernamePasswordAuthenticationToken,而是实现AuthenticationProvider接口,在supports()方法里表明当前类实现的是哪个AuthenticationToken类。 authenticate()方法里用来描述主要业务逻辑。
下面来看下authenticate()方法的业务逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication; SecurityUserDetails userDetails = (SecurityUserDetails) securityUserDetailsService.loadUserByUsername((String) authentication.getPrincipal()); if (!userDetails.getPassword().equals(authentication.getCredentials())) { // 自定义异常类,继承自RuntimeException throw new CommonException("密码错误"); } JWTUser jwtUser = new JWTUser(); jwtUser.setOpenId(userDetails.getOpenId()); jwtUser.setUserId(userDetails.getUserId()); jwtUser.setUsername(userDetails.getUsername()); token.setDetails(jwtUser); return new UsernamePasswordAuthenticationToken(jwtUser, null, userDetails.getAuthorities()); }
UserDetailsService是security获取用户信息的接口,需要我们自己去实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Service public class SecurityUserDetailsService implements UserDetailsService { @Autowired private UserDAO userDAO; @Autowired private UserRoleDAO userRoleDAO; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDAO.findByUsername(username); if (user == null) { return null; } List<UserRole> userRoles = userRoleDAO.findAllByUserId(user.getId()); return new SecurityUserDetails(user.getId(), user.getOpenId(), username, user.getPassword(), getUserAuthorities(userRoles)); } private List<GrantedAuthority> getUserAuthorities(List<UserRole> userRoles) { List<GrantedAuthority> authorities = new ArrayList<>(); for (UserRole userRole : userRoles) { authorities.add(new SimpleGrantedAuthority("ROLE_" + userRole.getRoleCode())); } return authorities; } }
最后返回的SecurityUserDetails类继承了security的User类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Getter @Setter public class SecurityUserDetails extends User { private Long userId; private String openId; public SecurityUserDetails(Long userId, String openId, String username, String password, Collection<? extends GrantedAuthority> authorities) { super(username, password, true, false, false, false, authorities); this.userId = userId; this.openId = openId; } } // User类构造方法之一 public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { if (username != null && !"".equals(username) && password != null) { this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); } else { throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); } }
User类的构造方法有多个参数,我们主要关注的是username,password,authorities, authorities参数用来存放登录用户的角色权限等信息,存放角色信息时要加上”ROLE_”开头让security知道这是一个角色,比如admin角色存入authorities是”ROLE_admin”
回过头看authenticate()方法的最后一行
1 return new UsernamePasswordAuthenticationToken(jwtUser, null, userDetails.getAuthorities());
返回了一个Authentication对象,先来看下该类的构造方法参数
1 2 3 4 5 6 public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); }
principal参数存放主要信息,这里使用JWTUser类封装了username,userId,openId等信息存入。credentials存放一些额外的信息。
生成token 获取到要用的Authentication对象后,用它来生成一个token,这里用的是jwt。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Component public class TokenProvider { // 配置文件中的签名,token时长等信息,可以直接在代理中写死(不建议) @Value("${jwt.secret_key}") private String secretKey; @Value("${jwt.token_validity}") private long tokenValidity; @Value("${jwt.token_validity_remember_me}") private long tokenValidityRememberMe; /** * 生成token */ public String createToken(Authentication authentication, Boolean rememberMe) { long now = (new Date()).getTime(); Date validity = rememberMe ? new Date(now + this.tokenValidityRememberMe * 1000) : new Date(now + this .tokenValidity * 1000); JWTUser authUser = (JWTUser) authentication.getPrincipal(); Long userId = authUser.getUserId(); String openId = authUser.getOpenId(); String username = authUser.getUsername(); return Jwts.builder().setSubject(authentication.getName()) .claim("userId", userId.toString()) .claim("openId", openId) .claim("username", username) .claim("roles", getRoleFromAuthorities(authentication.getAuthorities())) .signWith(SignatureAlgorithm.HS256, secretKey) .setExpiration(validity) .compact(); } private String getRoleFromAuthorities(Collection<? extends GrantedAuthority> authorities) { List<String> roles = new ArrayList<>(); for (GrantedAuthority authority : authorities) { if (authority instanceof SimpleGrantedAuthority) { roles.add(authority.getAuthority()); } } return String.join(",", roles); } }
createToken()方法就是用来生成token字符串返回给前端,之后请求其它接口时将token传入用来验证用户信息。比起存入session,jwt是一种以时间换空间的策略,又不会有存cookie跨域问题。
token验证 以上讲的主要是登录时用security生成token,在那之后使用该token请求接口时又要怎么解析并获取用户信息。一般在项目里是用过滤器或者拦截器进行拦截解析的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 public class JWTFilter extends GenericFilterBean { private TokenProvider tokenProvider; public JWTFilter(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String token = getTokenForHeader(httpServletRequest); if (StringUtils.hasText(token)) { if (tokenProvider.validateToken(token)) { Authentication authentication = tokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(servletRequest, servletResponse); } private String getTokenForHeader(HttpServletRequest request) { //获取header里的token String token = request.getHeader(“Authorization”); if (StringUtils.hasText(token)) { return token; } return null; } } @Component public class TokenProvider { /** * 获取 */ public Authentication getAuthentication(String token) { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); JWTUser jwtUser = null; String openId = claims.get("openId", String.class); Long userId = Long.valueOf(claims.get("userId").toString()); String username = claims.get("username", String.class); jwtUser = new JWTUser(userId, username, openId); return new UsernamePasswordAuthenticationToken(jwtUser, null, getAuthoritiesFromRole(claims.get("roles", String.class))); } /** * token验证 */ public boolean validateToken(String authToken) { try { Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken); } catch (Exception e) { return false; } return true; } private List<SimpleGrantedAuthority> getAuthoritiesFromRole(String roles) { List<SimpleGrantedAuthority> authorities = new ArrayList<>(); String[] roleArray = roles.split(","); for (String role : roleArray) { authorities.add(new SimpleGrantedAuthority(role)); } return authorities; } }
在Filter中,我们在请求头中获取用户token,先token合法性进行解析,解析成功后,像登录时生成一个Authentication对象存储。
doFIlter()方法里
1 SecurityContextHolder.getContext().setAuthentication(authentication);
之后要全局获取Authentication可以自己进行一层封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public final class SecurityUtils { public static JWTUser getCurrentUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { if (authentication.getPrincipal() instanceof JWTUser) { return (JWTUser) authentication.getPrincipal(); } } return null; } }
过滤器配置 过滤器顺序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class JWTConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private TokenProvider tokenProvider; public JWTConfigurer(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override public void configure(HttpSecurity http) throws Exception { JWTFilter jwtFilter = new JWTFilter(tokenProvider); http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); } }
过滤路径 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final TokenProvider tokenProvider; public SecurityConfig(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .disable() .headers() .frameOptions() .disable() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // yml配置的context-path: /base 不能加上 .antMatchers("/test/**").permitAll() .antMatchers("/user/signIn").permitAll() .antMatchers("/**").authenticated() .and() .exceptionHandling() .accessDeniedHandler(accessDeniedHandler()) .authenticationEntryPoint(authenticationEntryPoint()) .and() .apply(securityConfigurerAdapter()) ; } private JWTConfigurer securityConfigurerAdapter() { return new JWTConfigurer(tokenProvider); } @Bean public AccessDeniedHandler accessDeniedHandler() { return new AuthAccessDeniedHandler(); } @Bean public AuthenticationEntryPoint authenticationEntryPoint() { return new LoginAuthenticationEntryPoint(); } }
在Filter中如果解析token失败会造成请求500错误,无法正确返回我们要的信息, AuthAccessDeniedHandler类和LoginAuthenticationEntryPoint是针对其中两种异常返回自定义的异常信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class AuthAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); CommonResult result = new CommonResult("Prohibition of access"); result.setCode("403"); PrintWriter out = response.getWriter(); out.print(JSON.toJSONString(result)); out.flush(); out.close(); } }
角色注解拦截 在controller层的方法里加上@PreAuthorize进行权限拦截
1 2 3 4 5 @PreAuthorize("hasRole('admin')") @GetMapping("/userInfo") public CommonResult getUserInfo() { return CommonResultTemplate.execute(() -> userManager.getUserInfo()); }
@PreAuthorize(“hasRole(‘admin’)”)表示admin角色才能访问该接口,@PreAuthorize注解支出SPEL表达式