前言
SpringSecurity是一个功能强大且高度可定制的身份验证和访问控制的框架。
提供完善的认证机制和方法级的授权功能。
它的核心是一组过滤链,不同功能经由不同的过滤器。
JWT介绍
JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。
JWT组成
格式
JWT token
- header
- payload
- signature
header
header中存放签名的生成算法
{"alg": "HS512"}
payload
payload中存放用户名、token的生成时间和过期时间
{"sub":"admin","created":1489079981393,"exp":1489684781}
signature
signature为以header和payload生成的签名,一旦header和payload被篡改,验证则失败
//secret为加密算法的密钥
String signature = HMACSHA512(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
样例
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.diki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw
认证与授权原理
- 用户登录,成功后返回Token
- 登录后每次用户在调用接口时在header中添加Authorization的头,值为token
- 后台对request中Authorization信息解析校验获得用户信息,实现认证和授权
依赖
<!--JWT登录支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
工具类
在这里原作者将工具类交由了Spring管理,全局仅存在一个Utils
/**
* JwtToken生成工具类
*/
@Component
public class JwtTokenUtils {
//Token日志
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";
//Jwt生成密钥
@Value("${jwt.secret}")
private String secret;
//Jwt过期时间
@Value("${jwt.expiration}")
private Long expiration;
/**
*返回token过期时间
* @return token过期时间
*/
private Date generateExpirationDate(){
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 根据Claims生成JwtToken
* @param claims token附带的信息
* @return token字符串
*/
private String generateToken(Map<String,Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
}
/**
*从token获取JWT中的负载
* @param token token字符串
* @return JWT负载
*/
private Claims getClaimsFromToken(String token){
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
if (claims == null){
LOGGER.info("JWT格式验证失败:{}",token);
}
return claims;
}
/**
*根据token字符串解析返回username
* @param token token字符串
* @return username
*/
public String getUsernameFromToken(String token){
Claims claims = getClaimsFromToken(token);
String username = claims.getSubject();
return username;
}
/**
* 验证Token是否有效
* @param token Token字符串
* @param userDetails 数据库中用户信息
* @return 是否有效
*/
public boolean validateToken(String token, UserDetails userDetails){
String username = getUsernameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 根据Token判断是否已经失效
* @param token token字符串
* @return token是否失效
*/
private boolean isTokenExpired(String token){
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 根据token获取过期时间
* @param token token字符串
* @return token过期时间
*/
private Date getExpiredDateFromToken(String token){
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成Token
* @param userDetails 用户信息
* @return token字符串
*/
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);
}
/**
* 判断token是否可以刷新
* @param token Token字符串
* @return 是否可以刷新
*/
public boolean canRefresh(String token){
//没有过期的Token才可以被刷新
return !isTokenExpired(token);
}
/**
* 刷新token
* @param token token字符串
* @return 新的Token
*/
public String refreshToken(String token){
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
SpringSecurity
依赖
<!--SpringSecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
使用
SpringSecurity配置类
自定义SpringSecurity配置 extends WebSecurityConfigurerAdapter
否则会自动注入DefaultConfigurerAdapter类----The default configuration for web security
/**
* SpringSecurity配置类
*/
@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() //使用Jwt,不需要csrf
.disable()
.sessionManagement() //基于Token,所以不需要session
.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() //跨域请求会进行一次options请求
// .antMatchers("/**").permitAll() //测试时全部允许访问
.anyRequest() //除上面外的所有请求全部需要鉴权认证
.authenticated();
//禁用缓存
httpSecurity.headers().cacheControl();
//添加Jwt Filter
httpSecurity.addFilterBefore(jwtAuthentucationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
/**
* 配置UserDetailsService PasswordEncoder
*/
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
//配置UserDetailsService
public UserDetailsService userDetailsService(){
//获取用户登录信息
return username -> {
//根据username查询用户
UmsAdmin adminByUsername = umsAdminService.getAdminByUsername(username);
if (Objects.nonNull(adminByUsername)){
//根据userId获得PermissioonList
List<UmsPermission> permissionList = umsAdminService.getPermissionList(adminByUsername.getId());
return new AdminUserDetails(adminByUsername, permissionList);
}
throw new UsernameNotFoundException("用户名或密码错误");
};
}
/**
* Jwt过滤器
*/
@Bean
public JwtAuthentucationTokenFilter jwtAuthentucationTokenFilter(){
JwtAuthentucationTokenFilter jwtAuthentucationTokenFilter = new JwtAuthentucationTokenFilter();
return jwtAuthentucationTokenFilter;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
-
void configure(HttpSecurity httpSecurity)-
httpSecurity.antMatchers( //允许对网站静态资源的无授权访问 "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", //Swagger3 ...?位置 "/v2/api-docs/**" ).permitAll() -
//跨域请求会进行一次options请求 -- 允许 httpSecurity.antMatchers(HttpMethod.OPTIONS).permitAll() -
//添加Jwt Filter 注入的为下方@Bean注入的对象 httpSecurity.addFilterBefore(jwtAuthentucationTokenFilter(), UsernamePasswordAuthenticationFilter.class); -
//添加自定义未授权和未登录结果返回 response直接返回 在后面了 httpSecurity.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthenticationEntryPoint);
-
-
@Override /** * 配置UserDetailsService * 配置PasswordEncoder */ protected void configure(AuthenticationManagerBuilder auth) throws Exception { //注入的为下方@Bean注入的对象 auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); }
PasswordEncoder
在这里直接在SpringSecurity类内注入
//注入默认
//可以对密码进行加密 PasswordEncoder.encode(CharSequence rawPassword);
//可以用明文密码与加密密码进行对比
//PasswordEncoder.matches(CharSequence rawPassword, String encodedPassword);
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
UserDetailsService
直接在SpringSecurity类内注入了,因为只有一个接口,可以通过Lambda重写。
```java
//重写了UserDetailsService.loadUserByUsername(String username);
@Bean
//配置UserDetailsService
public UserDetailsService userDetailsService(){
//获取用户登录信息
return username -> {
//根据username查询用户
UmsAdmin adminByUsername = umsAdminService.getAdminByUsername(username);
if (Objects.nonNull(adminByUsername)){
//根据userId获得PermissioonList
List<UmsPermission> permissionList = umsAdminService.getPermissionList(adminByUsername.getId());
//AdminUserDetails implements UserDetails
//重写了UserDetails接口方法
return new AdminUserDetails(adminByUsername, permissionList);
}
throw new UsernameNotFoundException("用户名或密码错误");
};
}
```
UserDetails实现类
UserDetails规定了SpringSecurity需要的用户信息的方法,我们要对其进行重写
在这个项目中许多权限并没有用到,所以直接返回为true了
/**
* SpringSecurity需要的用户详情
*/
public class AdminUserDetails implements UserDetails {
//用户实体
private UmsAdmin umsAdmin;
//用户权限实体List
private List<UmsPermission> permissionList;
/**
* AdminUserDetails构造方法
* @param umsAdmin 用户实体
* @param permissionList 用户权限信息List
*/
public AdminUserDetails(UmsAdmin umsAdmin, List<UmsPermission> permissionList) {
this.umsAdmin = umsAdmin;
this.permissionList = permissionList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//返回当前用户的权限对象SimpleGrantedAuthority List
//SimpleGrantedAuthority内仅有属性role对应权限内容的字段
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层不再展示
package com.yancy0109.mall.service;
import com.yancy0109.mall.mbg.model.UmsAdmin;
import com.yancy0109.mall.mbg.model.UmsPermission;
import java.util.List;
/**
* 后台管理员Service
*/
public interface UmsAdminService {
/**
* 根据用户名获取后台管理员
* @param username 用户名
* @return UmsAdmin
*/
UmsAdmin getAdminByUsername(String username);
/**
* 注册功能
* @param umsAdminParam 注册UmsAdmin参数
* @return UmsAdmin
*/
UmsAdmin register(UmsAdmin umsAdminParam);
/**
* 登录
* @param username 用户名
* @param password 密码
* @return Token字符串
*/
String login(String username, String password);
/**
* 获取用户所有权限---角色权限和+-权限
* @param adminId
* @return
*/
List<UmsPermission> getPermissionList(Long adminId);
}
UmsPermission实体类
UserDetailsService实现类内通过调用自定义用户信息Service接口查询出UmsPermissionList,构造UserDetails实现类返回
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实现类返回
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内注入了,也可以通过其他注解注入配置好的组件
/**
* Jwt过滤器
*/
@Bean
public JwtAuthentucationTokenFilter jwtAuthentucationTokenFilter(){
JwtAuthentucationTokenFilter jwtAuthentucationTokenFilter = new JwtAuthentucationTokenFilter();
return jwtAuthentucationTokenFilter;
}
Jwt过滤器内细节内容
调用重写的UserDetails接口方法,返回用户信息
UserDetails UserDetailsService.loadUserByUsername(String username)
通过Token校验的请求将会设置到SecurityContextHolder
void SecurityContextHolder.getContext().setAuthentication(Authentication authentication);
Filter完整代码
/**
* Jwt登录授权过滤器
* 为什么用Filter而不是Inteceptor -- SpringSecurity实现
*/
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)){
//截取Bearer后
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 管理对应用户
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,
null,
userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
LOGGER.info("authenticated user: {}", username);
//设置到SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
-
@Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
自定义未授权返回
/**
* 当访问接口没有权限,自定义返回结果
*/
@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();
}
}
未登录结果返回
/**
* 当未登录或者token失效访问接口时,自定义返回结果
*/
@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类内注入
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
AuthenticationManager提供了方法
//实现类ProviderManager
Authentication authenticate(Authentication authentication) throws AuthenticationException;
AuthenticationManager.authenticate方法内,会遍历寻找可以处理的AuthenticationProvider,调用
//DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider
//AbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口
Authentication AuthenticationProvider.authenticate(Authentication authentication) throws AuthenticationException;
AuthenticationProvider.authenticate方法内,调用抽象方法retrieveUser
DaoAuthenticationProvider.retrieveUser方法内,调用UserDetailsService根据username进行查询返回UserDetails,调用方法检查UserDetails内是否可用,并返回最终结果
Controller接口
品牌管理接口
PmsBrandController
定义接口权限注解
@PreAuthorize("hasAuthority('pms:brand:update')")
注意:UserDetails获得的用户用户权限列表,应该包含这个权限字段(pms:brand:update),也就是要与数据库存储的权限信息格式对应
Collection<? extends GrantedAuthority> UserDetails.getAuthorities();
/**
* 品牌管理Controller
*/
@Api(tags = "PmsBrandController")
@RestController
@RequestMapping("/brand")
public class PmsBrandController {
@Autowired
PmsBrandService pmsBrandService;
//生成日志Logger对象
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
@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);
}
}