Spring Secuitry
前言
SpringSecurity是一个功能强大且高度可定制的身份验证和访问控制的框架。
提供完善的认证机制和方法级的授权功能。
它的核心是一组过滤链,不同功能经由不同的过滤器。
JWT介绍
JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。
JWT组成
格式
JWT token
header中存放签名的生成算法
payload
payload中存放用户名、token的生成时间和过期时间
1
| {"sub":"admin","created":1489079981393,"exp":1489684781}
|
signature
signature为以header和payload生成的签名,一旦header和payload被篡改,验证则失败
1 2
| String signature = HMACSHA512(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
|
样例
1
| eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.diki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw
|
认证与授权原理
- 用户登录,成功后返回Token
- 登录后每次用户在调用接口时在header中添加Authorization的头,值为token
- 后台对request中Authorization信息解析校验获得用户信息,实现认证和授权
依赖
1 2 3 4 5 6
| <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
|
工具类
在这里原作者将工具类交由了Spring管理,全局仅存在一个Utils
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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
|
@Component public class JwtTokenUtils { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtils.class);
private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}") private String secret;
@Value("${jwt.expiration}") private Long expiration;
private Date generateExpirationDate(){ return new Date(System.currentTimeMillis() + expiration * 1000); }
private String generateToken(Map<String,Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512,secret) .compact(); }
private Claims getClaimsFromToken(String token){ Claims claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); if (claims == null){ LOGGER.info("JWT格式验证失败:{}",token); } return claims; }
public String getUsernameFromToken(String token){ Claims claims = getClaimsFromToken(token); String username = claims.getSubject(); return username; }
public boolean validateToken(String token, UserDetails userDetails){ String username = getUsernameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); }
private boolean isTokenExpired(String token){ Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); }
private Date getExpiredDateFromToken(String token){ Claims claims = getClaimsFromToken(token); return claims.getExpiration(); }
public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); }
public boolean canRefresh(String token){ return !isTokenExpired(token); }
public String refreshToken(String token){ Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } }
|
SpringSecurity
依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
|
使用
SpringSecurity配置类
自定义SpringSecurity配置 extends WebSecurityConfigurerAdapter
否则会自动注入DefaultConfigurerAdapter类—-The default configuration for web 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 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
|
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled=true) public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private UmsAdminService umsAdminService; @Autowired private RestfulAccessDeniedHandler restfulAccessDeniedHandler; @Autowired private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf() .disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers( "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", "/v2/api-docs/**" ).permitAll() .antMatchers("/admin/login","/admin/register").permitAll() .antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest() .authenticated(); httpSecurity.headers().cacheControl(); httpSecurity.addFilterBefore(jwtAuthentucationTokenFilter(), UsernamePasswordAuthenticationFilter.class); httpSecurity.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthenticationEntryPoint); }
@Override public void configure(WebSecurity web) throws Exception { super.configure(web); }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); }
@Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
@Bean public UserDetailsService userDetailsService(){ return username -> { UmsAdmin adminByUsername = umsAdminService.getAdminByUsername(username); if (Objects.nonNull(adminByUsername)){ List<UmsPermission> permissionList = umsAdminService.getPermissionList(adminByUsername.getId()); return new AdminUserDetails(adminByUsername, permissionList); } throw new UsernameNotFoundException("用户名或密码错误"); }; }
@Bean public JwtAuthentucationTokenFilter jwtAuthentucationTokenFilter(){ JwtAuthentucationTokenFilter jwtAuthentucationTokenFilter = new JwtAuthentucationTokenFilter(); return jwtAuthentucationTokenFilter; }
@Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
|
- ```java
void configure(HttpSecurity httpSecurity)1 2 3 4 5 6 7 8 9 10 11 12 13 14
| - ```java httpSecurity.antMatchers( //允许对网站静态资源的无授权访问 "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", //Swagger3 ...?位置 "/v2/api-docs/**" ).permitAll()
|
```java
//跨域请求会进行一次options请求 – 允许
httpSecurity.antMatchers(HttpMethod.OPTIONS).permitAll()
1 2 3 4
| - ```java //添加Jwt Filter 注入的为下方@Bean注入的对象 httpSecurity.addFilterBefore(jwtAuthentucationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
|
```java
//添加自定义未授权和未登录结果返回 response直接返回 在后面了
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
1 2 3 4 5 6 7 8 9 10 11 12
| - ```java @Override /** * 配置UserDetailsService * 配置PasswordEncoder */ protected void configure(AuthenticationManagerBuilder auth) throws Exception { //注入的为下方@Bean注入的对象 auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); }
|
PasswordEncoder
在这里直接在SpringSecurity类内注入
1 2 3 4 5 6 7 8
|
@Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
|
UserDetailsService
直接在SpringSecurity类内注入了,因为只有一个接口,可以通过Lambda重写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ```java
@Bean
public UserDetailsService userDetailsService(){ return username -> { UmsAdmin adminByUsername = umsAdminService.getAdminByUsername(username); if (Objects.nonNull(adminByUsername)){ List<UmsPermission> permissionList = umsAdminService.getPermissionList(adminByUsername.getId()); return new AdminUserDetails(adminByUsername, permissionList); } throw new UsernameNotFoundException("用户名或密码错误"); }; } ```
|
UserDetails实现类
UserDetails规定了SpringSecurity需要的用户信息的方法,我们要对其进行重写
在这个项目中许多权限并没有用到,所以直接返回为true了
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
|
public class AdminUserDetails implements UserDetails { private UmsAdmin umsAdmin; private List<UmsPermission> permissionList;
public AdminUserDetails(UmsAdmin umsAdmin, List<UmsPermission> permissionList) { this.umsAdmin = umsAdmin; this.permissionList = permissionList; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return permissionList .stream().filter(permission -> Objects.nonNull(permission.getValue())) .map(permission -> new SimpleGrantedAuthority(permission.getValue())) .collect(Collectors.toList()); }
@Override public String getPassword() { return umsAdmin.getPassword(); }
@Override public String getUsername() { return umsAdmin.getUsername(); }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return umsAdmin.getStatus().equals(1); } }
|
UmsAdminService
用到的对用户信息查询的Service接口,具体实现与Dao层不再展示
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
| package com.yancy0109.mall.service;
import com.yancy0109.mall.mbg.model.UmsAdmin; import com.yancy0109.mall.mbg.model.UmsPermission;
import java.util.List;
public interface UmsAdminService {
UmsAdmin getAdminByUsername(String username);
UmsAdmin register(UmsAdmin umsAdminParam);
String login(String username, String password);
List<UmsPermission> getPermissionList(Long adminId); }
|
UmsPermission实体类
UserDetailsService实现类内通过调用自定义用户信息Service接口查询出UmsPermissionList,构造UserDetails实现类返回
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
| public class UmsPermission implements Serializable { private Long id;
@ApiModelProperty(value = "父级权限id") private Long pid;
@ApiModelProperty(value = "名称") private String name;
@ApiModelProperty(value = "权限值") private String value;
@ApiModelProperty(value = "图标") private String icon;
@ApiModelProperty(value = "权限类型:0->目录;1->菜单;2->按钮(接口绑定权限)") private Integer type;
@ApiModelProperty(value = "前端资源路径") private String uri;
@ApiModelProperty(value = "启用状态;0->禁用;1->启用") private Integer status;
@ApiModelProperty(value = "创建时间") private Date createTime;
@ApiModelProperty(value = "排序") private Integer sort;
private static final long serialVersionUID = 1L; }
|
UmsAdmin实体类
UserDetailsService实现类内通过调用自定义用户信息Service接口查询出UmsAdmin,构造UserDetails实现类返回
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
| public class UmsAdmin implements Serializable { private Long id;
private String username;
private String password;
@ApiModelProperty(value = "头像") private String icon;
@ApiModelProperty(value = "邮箱") private String email;
@ApiModelProperty(value = "昵称") private String nickName;
@ApiModelProperty(value = "备注信息") private String note;
@ApiModelProperty(value = "创建时间") private Date createTime;
@ApiModelProperty(value = "最后登录时间") private Date loginTime;
@ApiModelProperty(value = "帐号启用状态:0->禁用;1->启用") private Integer status;
private static final long serialVersionUID = 1L;
|
Jwt过滤器
这里直接在SpringSecurity内注入了,也可以通过其他注解注入配置好的组件
1 2 3 4 5 6 7 8
|
@Bean public JwtAuthentucationTokenFilter jwtAuthentucationTokenFilter(){ JwtAuthentucationTokenFilter jwtAuthentucationTokenFilter = new JwtAuthentucationTokenFilter(); return jwtAuthentucationTokenFilter; }
|
Jwt过滤器内细节内容
调用重写的UserDetails接口方法,返回用户信息
1
| UserDetails UserDetailsService.loadUserByUsername(String username)
|
通过Token校验的请求将会设置到SecurityContextHolder
1
| void SecurityContextHolder.getContext().setAuthentication(Authentication authentication);
|
Filter完整代码
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
|
public class JwtAuthentucationTokenFilter extends OncePerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthentucationTokenFilter.class); @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtils jwtTokenUtils;
@Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead;
@Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String authHeader = httpServletRequest.getHeader(this.tokenHeader); if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith(tokenHead)){ String authToken = authHeader.substring(this.tokenHead.length()); String username = jwtTokenUtils.getUsernameFromToken(authToken); LOGGER.info("checking username:{}",username); if (StringUtils.isNotBlank(username) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())){ UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtils.validateToken(authToken,userDetails)){ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); LOGGER.info("authenticated user: {}", username); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(httpServletRequest, httpServletResponse); } }
|
- ```java
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ### 自定义未授权返回
```java /** * 当访问接口没有权限,自定义返回结果 */ @Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); httpServletResponse.getWriter().println( JSONUtil.parse(CommonResult.forbiden()) ); httpServletResponse.getWriter().flush(); } }
|
未登录结果返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); httpServletResponse.getWriter().println( JSONUtil.parse(CommonResult.unauthorized()) ); httpServletResponse.getWriter().flush(); } }
|
AuthenticationManager
在这里直接在SpringSecurity类内注入
1 2 3 4 5
| @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
|
AuthenticationManager提供了方法
1 2
| Authentication authenticate(Authentication authentication) throws AuthenticationException;
|
AuthenticationManager.authenticate方法内,会遍历寻找可以处理的AuthenticationProvider,调用
1 2 3
|
Authentication AuthenticationProvider.authenticate(Authentication authentication) throws AuthenticationException;
|
AuthenticationProvider.authenticate方法内,调用抽象方法retrieveUser
DaoAuthenticationProvider.retrieveUser方法内,调用UserDetailsService根据username进行查询返回UserDetails,调用方法检查UserDetails内是否可用,并返回最终结果
Controller接口
品牌管理接口
PmsBrandController
定义接口权限注解
1
| @PreAuthorize("hasAuthority('pms:brand:update')")
|
注意:UserDetails获得的用户用户权限列表,应该包含这个权限字段(pms:brand:update),也就是要与数据库存储的权限信息格式对应
1
| Collection<? extends GrantedAuthority> UserDetails.getAuthorities();
|
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
|
@Api(tags = "PmsBrandController") @RestController @RequestMapping("/brand") public class PmsBrandController {
@Autowired PmsBrandService pmsBrandService;
private static final Logger LOGGER = LoggerFactory.getLogger(PmsBrandController.class);
@ApiOperation("获取所有品牌类别") @PreAuthorize("hasAuthority('pms:brand:read')") @GetMapping("/listAll") public CommonResult listAll(){ return CommonResult.success(pmsBrandService.listAllBrand()); }
@ApiOperation("添加品牌") @PreAuthorize("hasAuthority('pms:brand:create')") @PostMapping("/create") public CommonResult createBrand(PmsBrand pmsBrand){ boolean flag = pmsBrandService.createBrand(pmsBrand); if (flag){ return CommonResult.success(); }else { LOGGER.info("createBrand操作失败"); return CommonResult.failed(); } }
@ApiOperation("删除指定Id品牌信息") @PreAuthorize("hasAuthority('pms:brand:delete')") @PostMapping("/delete") public CommonResult deleteBrand(long id){ boolean flag = pmsBrandService.deleteBrand(id); if (flag){ return CommonResult.success(); }else { LOGGER.info("deleteBrand操作失败"); return CommonResult.failed(); } }
@ApiOperation("更新指定Id品牌信息") @PreAuthorize("hasAuthority('pms:brand:update')") @PostMapping("/update/{id}") public CommonResult updateBrand(@PathVariable("id") long id, PmsBrand pmsBrand){ boolean flag = pmsBrandService.updateBrand(id, pmsBrand); if (flag){ return CommonResult.success(); }else { LOGGER.info("updateBrand操作失败"); return CommonResult.failed(); } }
@ApiOperation("分页查询品牌信息") @PreAuthorize("hasAuthority('pms:brand:read')") @GetMapping("/list") public CommonResult pageBrand(@RequestParam(value = "pageNum",defaultValue = "1") int pageNum, @RequestParam(value = "paegSize",defaultValue = "3") int pageSize){ return CommonResult.success(pmsBrandService.listPmsBrand(pageNum,pageSize)); }
@ApiOperation("查询指定Id品牌信息") @PreAuthorize("hasAuthority('pms:brand:read')") @GetMapping("/{id}") public CommonResult getBrandById(@PathVariable("id") long id){ return CommonResult.success(pmsBrandService.getPmsBrand(id)); } }
|
用户功能接口
UmsAdminController
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
| @RestController @RequestMapping("/admin") @Api(tags = "UmsAdminController", description = "后台用户管理") public class UmsAdminController {
@Autowired private UmsAdminService umsAdminService; @Value("jwt.tokenHead") private String tokenHead; @Value("jwt.tokenHeader") private String tokenHeader;
@ApiOperation(value = "用户注册") @PostMapping("/register") public CommonResult register(UmsAdmin umsAdmin){ UmsAdmin umsAdminResult = umsAdminService.register(umsAdmin); if (Objects.isNull(umsAdminResult)){ return CommonResult.failed("注册失败"); } return CommonResult.success(umsAdminResult); }
@ApiOperation("登陆后返回Token") @PostMapping("/login") public CommonResult login(String username, String password){ if (StringUtils.isBlank(username) || StringUtils.isBlank(password)){ return CommonResult.validateFailed(); } String token = umsAdminService.login(username, password); if (StringUtils.isBlank(token)){ return CommonResult.validateFailed("用户名或密码错误"); } Map<String, String> tokenMap = new HashMap<>(); tokenMap.put("token",token); tokenMap.put("tokenHead",tokenHead); return CommonResult.success(tokenMap); }
@ApiOperation("获取权限列表") @GetMapping("/permission/{adminId}") public CommonResult getPermissionList(@PathVariable("adminId") long adminId){ List<UmsPermission> permissionList = umsAdminService.getPermissionList(adminId); return CommonResult.success(permissionList); } }
|