1

前言

作者从hallo world开始到目前的菜鸟水平,几乎接触过的每个项目都避不开登录这个功能。从最开始的不了解会话机制,密码账户校验成功只管往session里存个人信息,到后来的了解无状态、有状态的登陆的各个方案,不得不说学习得是一个积累得过程,想要完整的了解一门技术,一个技术栈,得系统的去学习。想起了之前面试过一个两年技术的小哥,不知道出于什么想法,他在简历的末尾来了4个饼图,以百分比的形式去展示掌握程度,当时没忍住笑了出来(当然不是面试的时候)——java 75%,mysql80%,orcle75%,linux80%,面试一问你看过hashmap的源码,了解底层原理吗?额~...没有,不知道。那你用过触发器,知道什么是存储过程吗?额~...没有用过...

其实这种情况很多见,一些年轻的求职者找工作时用的简历都很粗糙,包括作者自己的。但是一定要注意对于自己会的技术栈,你掌握的程度一定要用对关键词。比如一个一年技术经验的java开发,简历上写着各种掌握、熟练掌握springboot,稍微好一点的公司应该会直接把你刷掉,缺人的公司可能会约面试,但是面试官一般都会很反感。当然如果天赋异禀,真的能在这么短的时间内达到掌握的程度,那我也无话可说,建议直接投aliP8。

1、方案的选择

登录身份存储的方案其实很多,但出于很多的项目都需要去考虑项目的安全和权限体系,现在流行的权限框架有shiro,和springsecurity,前者相对用户群体会大一些。而登录整体分为两大类:无状态和有状态。

  • 有状态:

    有状态是指用户与服务器进行会话,使用session等方式保持,服务端会存留用户的会话信息。

  • 无状态:

    而无状态是现在比较流行的方式,通俗的说,服务器不去保留你用户会话的信息,用户来访通过认证授权之后给你的token,存于用户端,服务端会从token中解析其中的信息,然后根据这些信息去做业务操作。在解析token之前,并不能知道你是谁,服务端也不保留token,不使用session,在访问量比较大的情况下会减小服务器压力。

因此登录方案的选择主要还是看具体的项目规模,业务的需求。

本文(本项目)主要介绍的是Spring security结合jwt、redis去实现登录。

因为业务需求,作者在无状态的基础上,将token进行了唯一处理,意味着单点登录,单token有效处理,采用redis去实现。因此,打破了无状态。主要是因为单一的jwt的认证方式有一些缺点:

  1. token生成之后无法强迫失效,只能自动过期;
  2. token无法延时;
  3. 修改用户信息后无法同步;
  4. token不能存储太多信息(作者曾将一整个对象实体,包括所有对象权限对象解析成token,结果request报token过长不让用);
  5. 基于第一点有个情况是用户多次登录能够登录成功,意味着服务器会发放多次token,且这些token都是有效的;

2、Spring security简介

spring security 的核心功能主要包括:

  • 认证 (你是谁)
  • 授权 (你能干什么)
  • 攻击防护 (防止伪造身份)

spring security 可以和shiro一样实现权限的控制,且相比shiro功能会更强大。Spring Security可以为 Spring 应用提供声明式的安全访问控制,通过一系列在spring应用上下文中可配置的bean,利用spring ioc 和aop 等功能特性来提供声明式的安全访问控制功能(通过注解来控制接口和方法的访问权限),减少重复的工作。

3、jwt

JSON Web Token (JWT),是在网络应用间传递信息的一种基于 JSON的开放标准,用户JSON对象在不同系统中进行信息的安全传输,主要使用场景用于用户身份提供者和服务提供者间传递被认证的用户身份信息。

对于Spring security和jwt本文就不做过多的介绍了,着重于实现。

4、开始实现

1) pom导入:

<!-- 此处仅展示security与jwt -->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
 <groupId>io.jsonwebtoken</groupId>
 <artifactId>jjwt</artifactId>
 <version>0.9.1</version>
</dependency>

2) 开始配置文件:

SecurityConfig:

package com.ssk.shop.config.security;
​
import com.ssk.shop.config.security.token.JwtTokenUtil;
import com.ssk.shop.config.security.token.RestAuthenticationEntryPoint;
import com.ssk.shop.config.security.token.RestfulAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.HttpFirewall;
​
import javax.sql.DataSource;
​
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
​
 @Autowired
 private BCryptPasswordEncoder passwordEncoder;
​
 /**
 * 注册没有权限的处理器
 */
 @Bean
 public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
 return new RestfulAccessDeniedHandler();
 }
​
 @Bean
 public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
 return new RestAuthenticationEntryPoint();
 }
​
 @Bean
 public BCryptPasswordEncoder passwordEncoder() {
 return new BCryptPasswordEncoder();
 }
​
 // 认证用户的来源(内存或者数据库)
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder);
 }
​
 //配置springSecurity相关信息artisanType/get_all
 protected void configure(HttpSecurity http) throws Exception {
 ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
 //不需要保护的资源路径允许访问
 for (String url : ignoreUrlsConfig().getUrls()) {
 registry.antMatchers(url).permitAll();
 }
 //允许跨域请求的OPTIONS请求
 registry.antMatchers(HttpMethod.OPTIONS)
 .permitAll();
 // 任何请求需要身份认证
 // 释放静态资源,指定资源拦截规则,指定自定义认证页面,指定退出认证配置,csrf配置
 registry.and()
 .authorizeRequests()
 .anyRequest()
 .authenticated()
 // 关闭跨站请求防护及不使用session
 .and()
 .csrf()
 .disable()
 .sessionManagement()
 .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
 // 自定义权限拒绝处理类
 .and()
 .exceptionHandling()
 .accessDeniedHandler(restfulAccessDeniedHandler())
 .authenticationEntryPoint(restAuthenticationEntryPoint())
 // 自定义权限拦截器JWT过滤器
 .and()
 .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
 }
​
 @Autowired
 DataSource dataSource;
​
 @Bean
 public PersistentTokenRepository getPersistentTokenRepository() {
 JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
 jdbcTokenRepository.setDataSource(dataSource);
//        jdbcTokenRepository.setCreateTableOnStartup(true);
 return jdbcTokenRepository;
 }
​
 @Bean
 public IgnoreUrlsConfig ignoreUrlsConfig() {
 return new IgnoreUrlsConfig();
 }
​
 @Bean
 public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
 return new JwtAuthenticationTokenFilter();
 }
​
 @Bean
 public JwtTokenUtil jwtTokenUtil() {
 return new JwtTokenUtil();
 }
​
 @Bean
 public HttpFirewall httpFirewall() {
 return new DefaultHttpFirewall();
 }
​
}

JwtTokenUtil:

package com.ssk.shop.config.security.token;
​
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
​
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
​
/**
 * jwt token生成工具
 *
 * @author ssk
 * @since 2020-11-03 08:09:00
 */
public class JwtTokenUtil {
 private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.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;
 @Value("${jwt.tokenHead}")
 private String tokenHead;
​
 /**
 * 根据负责生成JWT的token
 */
 private String generateToken(Map<String, Object> claims) {
 return Jwts.builder()
 .setClaims(claims)
 .setExpiration(generateExpirationDate())
 .signWith(SignatureAlgorithm.HS512, secret)
 .compact();
 }
​
 /**
 * 从token中获取JWT中的负载
 */
 private Claims getClaimsFromToken(String token) {
 Claims claims = null;
 try {
 claims = Jwts.parser()
 .setSigningKey(secret)
 .parseClaimsJws(token)
 .getBody();
 } catch (Exception e) {
 LOGGER.info("JWT格式验证失败:{}", token);
 }
 return claims;
 }
​
 /**
 * 生成token的过期时间
 */
 private Date generateExpirationDate() {
 return new Date(System.currentTimeMillis() + expiration * 1000);
 }
​
 /**
 * 从token中获取登录用户名
 */
 public String getUserNameFromToken(String token) {
 String username;
 try {
 Claims claims = getClaimsFromToken(token);
 username = claims.getSubject();
 } catch (Exception e) {
 username = null;
 }
 return username;
 }
​
 /**
 * 验证token是否还有效
 *
 * @param token       客户端传入的token
 * @param userDetails 从数据库中查询出来的用户信息
 */
 public boolean validateToken(String token, UserDetails userDetails) {
 String username = getUserNameFromToken(token);
 return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
 }
​
 /**
 * 判断token是否已经失效
 */
 private boolean isTokenExpired(String token) {
 Date expiredDate = getExpiredDateFromToken(token);
 return expiredDate.before(new Date());
 }
​
 /**
 * 从token中获取过期时间
 */
 private Date getExpiredDateFromToken(String token) {
 Claims claims = getClaimsFromToken(token);
 return claims.getExpiration();
 }
​
 /**
 * 根据用户信息生成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 oldToken 带tokenHead的token
 */
 public String refreshHeadToken(String oldToken) {
 if(StrUtil.isEmpty(oldToken)){
 return null;
 }
 String token = oldToken.substring(tokenHead.length());
 if(StrUtil.isEmpty(token)){
 return null;
 }
 //token校验不通过
 Claims claims = getClaimsFromToken(token);
 if(claims==null){
 return null;
 }
 //如果token已经过期,不支持刷新
 if(isTokenExpired(token)){
 return null;
 }
 //如果token在30分钟之内刚刷新过,返回原token
 if(tokenRefreshJustBefore(token,30*60)){
 return token;
 }else{
 claims.put(CLAIM_KEY_CREATED, new Date());
 return generateToken(claims);
 }
 }
​
 /**
 * 判断token在指定时间内是否刚刚刷新过
 * @param token 原token
 * @param time 指定时间(秒)
 */
 private boolean tokenRefreshJustBefore(String token, int time) {
 Claims claims = getClaimsFromToken(token);
 Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
 Date refreshDate = new Date();
 //刷新时间在创建时间的指定时间内
 if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){
 return true;
 }
 return false;
 }
}

jwt 过滤器:

package com.ssk.shop.config.security;
​
import com.ssk.shop.config.security.token.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;
​
/**
 * jwt 过滤器
 *
 * @author ssk
 * @since 2020-11-03 08:09:00
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
 @Resource
 private UserDetailsService userDetailsService;
 @Autowired
 private JwtTokenUtil jwtTokenUtil;
 @Value("${jwt.tokenHeader}")
 private String tokenHeader;
 @Value("${jwt.tokenHead}")
 private String tokenHead;
​
 @Override
 protected void doFilterInternal(HttpServletRequest request,
 HttpServletResponse response,
 FilterChain chain) throws ServletException, IOException {
 String authHeader = request.getHeader(this.tokenHeader);
​
 if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
 String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
 String username = jwtTokenUtil.getUserNameFromToken(authToken);
 LOGGER.info("checking username:{}", username);
 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
 UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
 if (jwtTokenUtil.validateToken(authToken, userDetails)) {
 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
 LOGGER.info("authenticated user:{}", username);
 SecurityContextHolder.getContext().setAuthentication(authentication);
 }
 }
 }
 chain.doFilter(request, response);
 }
}

此处如果使用的是单项目多类型用户,且都想进行登录控制,(admin可能涉及到很多的角色和权限控制,而用户端并不会设计,而是采用单用户角色)可以将admin和user(其他用户)都继承security的UserDetailsService,然后登录之后的UserDetail进行重新封装,最好是能放入角色,然后jwt验证的时候根据id拿出Userdetail信息中的角色来进行相对应的层重新授权,实例代码:

//将继承了UserDetail的类都注入
 @Resource
 private IAdminInfoFacade adminDetailsService;
 @Resource
 private IUserInfoFacade userDetailsFacade;
 
​
​
​
 @Override
 protected void doFilterInternal(HttpServletRequest request,
 HttpServletResponse response,
 FilterChain chain) throws ServletException, IOException {
 String authHeader = request.getHeader(this.tokenHeader);
 if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
 String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
 if (jwtTokenUtil.validateTokenSingle(authToken)){
 String username = jwtTokenUtil.getUserNameFromToken(authToken);
 //根据token解析完之后拿到角色 ADMIN/USER
 String role = jwtTokenUtil.getUserRoleFromToken(authToken);
 LOGGER.info("checking username:{}", username);
 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
 UserDetails userDetails = null;
 //更具角色来选择进入那个实现类进行重新授权校验
 if (role.equals(UserRoleTypeEnum.ADMIN.getType()) || role.equals(UserRoleTypeEnum.SUPER.getType())){
 userDetails = this.adminDetailsService.loadUserByUsername(username);
 }else if (role.equals(UserRoleTypeEnum.USER.getType())){
 userDetails = this.userDetailsFacade.loadUserByUsername(username);
 }
 if (jwtTokenUtil.validateToken(authToken, userDetails)) {
 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
 LOGGER.info("authenticated user:{}", username);
 SecurityContextHolder.getContext().setAuthentication(authentication);
 System.out.println("//");
 }
 }
 }
 }
 

但是这种方法处理建议不去使用security自带的登录,而是采用每种角色进行登录接口的实现。

自定义未授权返回:

package com.ssk.shop.config.security.token;
​
import cn.hutool.json.JSONUtil;
import com.ssk.utils.CommonResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
​
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
/**
 * 自定义返回结果:没有权限访问时
 * @author ssk
 * @since 2020-11-03 08:08:40
 */
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
 @Override
 public void handle(HttpServletRequest request,
 HttpServletResponse response,
 AccessDeniedException e) throws IOException, ServletException {
 response.setHeader("Access-Control-Allow-Origin", "*");
 response.setHeader("Cache-Control","no-cache");
 response.setCharacterEncoding("UTF-8");
 response.setContentType("application/json");
 response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
 response.getWriter().flush();
 }
}

自定义未登录/未认证返回:

package com.ssk.shop.config.security.token;
​
import cn.hutool.json.JSONUtil;
import com.ssk.utils.CommonResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
​
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
/**
 * 自定义返回结果:未登录或登录过期
 * @author ssk
 * @since 2020-11-03 08:08:40
 */
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
 @Override
 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
 response.setHeader("Access-Control-Allow-Origin", "*");
 response.setHeader("Cache-Control","no-cache");
 response.setCharacterEncoding("UTF-8");
 response.setContentType("application/json");
 response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
 response.getWriter().flush();
 }
}

jwt 的基础配置

jwt:
 tokenHeader: Authorization #JWT存储的请求头
 secret: jwt_key #JWT加解密使用的密钥
 expiration: 604800 #JWT的超期限时间(60*60*24)
 tokenHead: Bearer  #JWT负载中拿到开头
 
#security不用授权的放行名单  需要结合上述的security配置文件
secure:
 ignored:
 urls: #安全路径白名单
 - /swagger-ui.html
 - /swagger-resources/**
 - /swagger/**
 - /**/v2/api-docs
 - /webjars/springfox-swagger-ui/**
 - /actuator/**
 - /druid/**
 - /admin-info/login
 - /admin/register
 - /admin/info
 - /admin/logout

以上便是所有的基础配置,jwt的密钥以及头我是直接放入配置文件中。

5、登录测试

image

此处使用swagger直接进行测试,使用的登录方法是自己进行重写

@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
​
@ApiOperation(value = "登录以后返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public CommonResult<Map<String, String>> login(@RequestBody UserLoginVO user) {
 try {
 String token = adminInfoFacade.login(user.getUsername(), user.getPassword());
 Map<String, String> tokenMap = new HashMap<>();
 tokenMap.put("token", token);
 tokenMap.put("tokenHead", tokenHead);
 return CommonResult.success(tokenMap);
 }catch (ServiceException e){
 return CommonResult.failed(e.getMessage());
 }
}

将security的loadUserByUsername进行重写,然后自定义一个登陆类LoginMan用于保存账号信息:

private PasswordEncoder passwordEncoder;
​
 @Resource
 private JwtTokenUtil jwtTokenUtil;
​
 @Override
 public String login(String username, String password) {
 String token = null;
 try {
 LoginMan userDetails = this.loadUserByUsername(username);
 if(!passwordEncoder.matches(password,userDetails.getPassword())){
 throw new BadCredentialsException("密码不正确");
 }
 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
 SecurityContextHolder.getContext().setAuthentication(authentication);
 token = jwtTokenUtil.generateToken(userDetails);
 } catch (AuthenticationException e) {
 LOGGER.warn("登录异常:{}", e.getMessage());
 throw new ServiceException("用户名或密码错误");
 }
 return token;
 }

LoginMan:根据自己的业务需求进行改写,主要实现对应的接口:

package com.ssk.shop.dto;
​
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
​
import java.util.ArrayList;
import java.util.List;
​
/**
 * 登录用户实体
 *
 * @author ssk
 * @since 2020-11-03 08:09:00
 */
public class LoginMan implements GrantedAuthority, UserDetails {
 /**
 * 实体id
 */
 private String id;
​
​
 /**
 * 密码
 */
 private String password;
​
 private Object manMsg;
​
 private String roleType;
​
 /**
 * 名称
 */
 private String name;
​
 private List<String> permissions;
​
 private boolean enabled = false;
​
 public Object getManMsg() {
 return manMsg;
 }
​
 public void setManMsg(Object manMsg) {
 this.manMsg = manMsg;
 }
​
 @Override
 @JsonIgnore
 public boolean isAccountNonExpired() { // 帐户是否过期
 return true;
 }
​
 @Override
 @JsonIgnore
 public boolean isAccountNonLocked() { // 帐户是否被冻结
 return true;
 }
​
 // 帐户密码是否过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
 @Override
 @JsonIgnore
 public boolean isCredentialsNonExpired() {
 return true;
 }
​
 @Override
 public boolean isEnabled() {
 return enabled;
 }
​
 public void setPassword(String password) {
 this.password = password;
 }
​
 public String getName() {
 return name;
 }
​
 public void setName(String name) {
 this.name = name;
 }
​
 public void setEnabled(boolean enabled) {
 this.enabled = enabled;
 }
​
 public String getRoleType() {
 return roleType;
 }
​
 public void setRoleType(String roleType) {
 this.roleType = roleType;
 }
​
 public List<String> getPermissions() {
 return permissions;
 }
​
 public String getId() {
 return id;
 }
​
 public void setId(String id) {
 this.id = id;
 }
​
​
 @Override
 @JsonIgnore
 public List<GrantedAuthority> getAuthorities() {
 List<GrantedAuthority> authorities = new ArrayList<>();
 authorities.add(new SimpleGrantedAuthority("ROLE_" + roleType));
 for (String permission : permissions) {
 authorities.add(new SimpleGrantedAuthority(permission));
 }
 return authorities;
 }
​
 @Override
 public String getPassword() {
 return password;
 }
​
 @Override
 public String getUsername() {
 return name;
 }
​
 @Override
 @JsonIgnore
 public String getAuthority() {
 return null;
 }
​
 public LoginMan(String name, String password, String roleType, List<String> permissions, String id , Object manMsg) {
 this.name = name;
 this.password = password;
 this.roleType = roleType;
 this.permissions = permissions;
 this.id = id;
 this.manMsg = manMsg;
 }
​
 public LoginMan() {
​
 }
​
 @Override
 public String toString() {
 return "LoginMan{" +
 "id='" + id + ''' +
 ", password='" + password + ''' +
 ", manMsg=" + manMsg +
 ", roleType='" + roleType + ''' +
 ", name='" + name + ''' +
 ", permissions=" + permissions +
 ", enabled=" + enabled +
 '}';
 }
}

登录:

#成功
{
 "code": 200,
 "message": "操作成功",
 "data": {
 "tokenHead": "Bearer",
 "token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJmcmlkYXkiLCJjcmVhdGVkIjoxNjA0NzA5NzkyMjI2LCJleHAiOjE2MDUzMTQ1OTJ9.EHoNsN1W1Q3sek64XGV0ubIXhnTBpcvjAezbzDOLQAhQnCzvO9f4ovCJUvtrLyzSexqvJcoOR8ccBEPd6HXFOw"
 }
}

#失败
{
 "code": 500,
 "message": "用户名或密码错误",
 "data": null
}

image

然后将token给swagger全局(token需要加上头,且与值之间用空格);

对需要权限的接口进行访问:

@PreAuthorize("hasAuthority('ADMIN_SEE')")
@RequestMapping("/query_page")
@ApiOperation(value = "查询管理员集合", notes = "查询管理员集合", httpMethod = "GET")
@ApiImplicitParams({
 @ApiImplicitParam(name = "roleId", value = "角色ID", paramType = "query", dataType = "String"),
 @ApiImplicitParam(name = "account", value = "账号", paramType = "query", dataType = "String"),
 @ApiImplicitParam(name = "name", value = "名字", paramType = "query", dataType = "String"),
 @ApiImplicitParam(name = "sex", value = "性别(0:男,1:女)", paramType = "query", dataType = "int"),
 @ApiImplicitParam(name = "enable", value = "状态(1是启用,0是禁用))", paramType = "query", dataType = "int"),
 @ApiImplicitParam(name = "phone", value = "电话", paramType = "query", dataType = "String"),
 @ApiImplicitParam(name = "page", value = "展示的页数", required = true, paramType = "query", dataType = "int",example = "1"),
 @ApiImplicitParam(name = "pageSize", value = "每页展示几条", required = true, paramType = "query", dataType = "int",example = "10")
})
public CommonResult<CommonPage<AdminListDto>> selectAdmin(@RequestParam(required = false) String roleId,
 @RequestParam(required = false) String account,
 @RequestParam(required = false) String name,
 @RequestParam(required = false) Integer sex,
 @RequestParam(required = false) Integer enable,
 @RequestParam(required = false) String phone,
 Integer page, Integer pageSize) {
 try {
 return CommonResult.success(adminInfoFacade.selectAdmin(roleId, account, name, sex, enable, phone,  page, pageSize));
 } catch (ServiceException e) {
 e.printStackTrace();
 return CommonResult.failed(e.getMessage());
 }
}

成功获取返回值:

{
 "code": 200,
 "message": "操作成功",
 "data": {
 "total": 0,
 "size": 10,
 "pages": 0,
 "current": 1,
 "records": [
 {
 "adminInfoId": "053d9349b5ca4c56a873053c2afbc6df",
 "account": "bjty",
 "password": "$2a$10$jSw6APBmhUzkNuSVFKEGR.yu/srrbwHUimpvllm1QZB9ifMf1wXry",
 "adminRole": "fcc5a1585f5b4e32bc283a3e232435c0",
 "roleName": "管理员2号",
 "adminDepartmentName": null,
 "name": "白开水",
 "sex": null,
 "phone": "18999999999",
 "enableState": 1,
 "createTime": "2020-08-25T02:28:47.000+0000"
 },

清空token后进行访问:

{
 "code": 401,
 "data": "Full authentication is required to access this resource",
 "message": "暂未登录或token已经过期"
}

总结:

项目中初期设定是后端管理员可以拥有单角色,然后给角色进行权限分类,接口访问通过权限或角色进行控制。个人的理解是security将账号的权限以及角色在登录时放入了session(启用的情况下),然后通过会话访问时获取session中的会话信息,从而拿到权限进行比较,满足了便可放行访问。

security通过aop的方式进行权限校验,通过注解进行权限比较

@PreAuthorize("hasAnyRole('ROLE_ADMIN')")//角色

@PreAuthorize("hasAuthority('ADMIN_SEE')")//权限

其中对于角色,我们需要在登录时将角色初始化如权限集合时添加上“ROLE_”,用以标识角色。

public LoginMan(String name, String password, String roleType, List<String> permissions, String id , Object manMsg) {
 this.name = name;
 this.password = password;
 this.roleType = roleType;
 this.permissions = permissions;
 this.id = id;
 this.manMsg = manMsg;
}
​
@Override
@JsonIgnore
public List<GrantedAuthority> getAuthorities() {
 List<GrantedAuthority> authorities = new ArrayList<>();
 authorities.add(new SimpleGrantedAuthority("ROLE_" + roleType));
 for (String permission : permissions) {
 authorities.add(new SimpleGrantedAuthority(permission));
 }
 return authorities;
}

本文中实例化LoginMan(UserDetail)是将角色类型单独作为参数传入。

如果未禁用session,还可以通过获取下列方式获取session中的值:

/**
 * 获取当前账户id
 * @return
 * @throws ServiceException
 */
public String getThisAccountId()throws ServiceException {
 try {
 Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
 if (ObjectHelper.isNotEmpty(principal) && principal.equals("anonymousUser")){
 return null;
 }else {
 LoginMan thisMan = (LoginMan) principal;
 return thisMan.getId();
 }
 }catch (Exception e){
 return null;
 }
}

后期将会禁用session,采用token保存的方式进行实现。


物干焯的小次郎
9 声望0 粉丝