前言
单角色的意思就是,一个用户只有一个角色,用户与角色处于一对一关系,因此,角色可以放到用户表的字段中冗余。在一些小型项目中,需求也许没有那么复杂,因此只考虑单角色。
以下项目的 gitee 地址,便于需要时直接 clone:
创建数据表
建立一个数据库,复制到 mysql 命令行执行就行了。本文是使用 MySQL 的 test 数据库。
CREATE TABLE `sys_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) NULL,
`password` varchar(255) NULL,
`role` varchar(255) NULL,
PRIMARY KEY (`id`)
);
Maven 工程 pom.xml 文件
配置可参考
SpringBoot 配置文件
配置一下数据源即可。IP、端口、数据库名、用户名、密码根据需要改。
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: root
POJO 与常量
UserDO.java
sys_user 的 Java 实体类
@TableName(value = "sys_user")
@Getter
@Setter
public class UserDO {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "username")
private String username;
@TableField(value = "password")
private String password;
@TableField(value = "role")
private String role;
}
ResponseCode.java
ResponseVO.java
ResponseVO 为返回给前端的包裹对象;ResponseCode 进行了返回码的定义。具体可见:
LoginDTO.java
前端传递给后端的对象
@Getter
@Setter
public class LoginDTO {
private String username;
private String password;
}
LoginVO.java
后端传递给前端的对象
@Getter
@Setter
public class LoginVO {
private String token;
}
Mapper
UserMapper.java
只需要继承 MybatisPlus 的 BaseMapper 即可
@Repository
public interface UserMapper extends BaseMapper<UserDO> {
}
SpringBeanUtil
为了能够在没有被 Spring 容器管理的对象中获取 Bean,自定义容器工具,可参考如下文章代码:
JsonWebToken
Shiro 框架提供的 UsernamePasswordToken 已经不适用了,JWT 是不存储敏感等敏感信息的。自己实现一个。Credentials 目前不返回任何有效值,直接返回 null。
public class JsonWebToken implements HostAuthenticationToken {
private String username;
private String host;
public JsonWebToken(String username, String host) {
super();
this.username = username;
this.host = host;
}
@Override
public Object getPrincipal() {
return username;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public String getHost() {
return host;
}
}
JwtFilter
JwtFilter 是当用户请求非匿名权限的 URL 时会执行的过滤器。
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 每个请求都会先判断 isAccessAllowed(),该方法返回 false,之后会走 onAccessDenied()
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
/**
* super 方法会先 createToken,使用该 token 进行登录
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
super.executeLogin(request, response);
return true;
}
/**
* 重写 createToken,根据 JWT 头部获取
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 获取 IP 地址
String host = request.getRemoteHost();
// 获取 JWT 头部值,如果为空,则 username 以空值返回
String jwtHeader = httpServletRequest.getHeader("X-Token");
if(jwtHeader == null || jwtHeader.length() == 0) {
return new JsonWebToken("", host);
}
// 如果 header 不为空,那么尝试解析 username
String username = JWT.decode(jwtHeader).getClaim("username").asString();
if (username == null || username.length() == 0) {
return new JsonWebToken("", host);
}
return new JsonWebToken(username, host);
}
@Override
protected boolean preHandle(ServletRequest req, ServletResponse res) throws Exception {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
response.setStatus(HttpStatus.OK.value());
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
request.getHeader(HttpHeaders.ORIGIN));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
request.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
}
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
request.getHeader(HttpHeaders.ORIGIN));
return super.preHandle(request, response);
}
}
Realm
Realm 是 Shiro 中的概念,当执行 JWTFilter.executeLogin() 方法中的 getSubject().login() 时,会进入Realm 进行认证信息与权限信息获取。
@Component
public class DefaultAuthorizingRealm extends AuthorizingRealm {
@Autowired
UserMapper userMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
/* 取得本用户的 username */
UserDO user = (UserDO)SecurityUtils.getSubject().getPrincipal();
/* 添加 role,这里只有 1 个 role */
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole(user.getRole());
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 这里的 token 其实是 JsonWebToken 的实例
String username = token.getPrincipal().toString();
// 根据 username 查询系统用户
UserDO user = userMapper.selectOne(new QueryWrapper<UserDO>().lambda().eq(UserDO::getUsername, username));
if (user == null) {
throw new UnknownAccountException("用户不存在");
}
return new SimpleAuthenticationInfo(user, null, getName());
}
}
ShiroConfiguration
注意:此处代码在导入包时可能会报错,因为 Shiro 中使用的是 org.apache.shiro.mgt.SecurityManager,而编译器默认会使用 java.lang.SecurityManager,需要手动导入。
import org.apache.shiro.mgt.SecurityManager;
@Configuration
public class ShiroConfiguration {
@Autowired
DefaultAuthorizingRealm realm;
/**
* 生成一个随机值用户 JWT 的加密
*
* @return 用于 JWT 加密的随机值
*/
@Bean
public String secret() {
return UUID.randomUUID().toString();
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
/* 定义过滤器链 */
LinkedHashMap<String, String> filterChain = new LinkedHashMap<>();
/* 定义匿名用户即可访问的 URL,anon 意思是 anonymous */
filterChain.put("/user/login", "anon");
filterChain.put("/doc.html", "anon");
filterChain.put("/**/**.js", "anon");
filterChain.put("/**/**.css", "anon");
filterChain.put("/swagger-resources/**", "anon");
filterChain.put("/webjars/**", "anon");
filterChain.put("/v2/**", "anon");
/* jwt 必须放在最后 */
filterChain.put("/**", "jwt");
/* 添加 JWT 过滤器,并命名为 jwt */
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChain);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
/* 关闭shiro自带的session */
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
SwaggerConfiguration
如果不需要前后端在线文档,可以跳过这一步。
@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfiguration {
public List<Parameter> globalOperationParameters() {
List<Parameter> params = new ArrayList<Parameter>();
Parameter parameter = new ParameterBuilder()
.name("X-Token")
.description("JSON Web Token")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build();
params.add(parameter);
return params;
}
@Bean
public Docket Api() {
ApiInfo apiInfo = new ApiInfoBuilder()
.build();
return new Docket(DocumentationType.SWAGGER_2)
.globalOperationParameters(globalOperationParameters())
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
.paths(PathSelectors.any())
.build();
}
}
UserService
UserService 接口
public interface UserService {
LoginVO login(LoginDTO loginDTO);
}
UserServiceImpl 实现类
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
String secret;
@Override
public LoginVO login(LoginDTO loginDTO) {
UserDO user = userMapper
.selectOne(new QueryWrapper<UserDO>().lambda().eq(UserDO::getUsername, loginDTO.getUsername()));
/* (1) 判断用户名是否存在 */
boolean exist = user != null;
if (!exist) {
throw new UnknownAccountException("用户名不存在");
}
/* (2) 判断密码是否错误 */
boolean valid = user.getPassword().equals(loginDTO.getPassword());
if (!valid) {
throw new IncorrectCredentialsException("密码错误");
}
/* (3) 获取该 user 的角色 */
String role = user.getRole();
String token = JWT.create()
.withClaim("username", user.getUsername())
.withClaim("role", role)
.withExpiresAt(DateUtil.offsetSecond(new Date(), 60))
.sign(Algorithm.HMAC256(secret));
LoginVO loginVO = new LoginVO();
loginVO.setToken(token);
return loginVO;
}
}
创建 UserController
@RestController
@Api(tags = "用户管理")
public class UserController {
@Autowired
UserService userService;
@PostMapping("/user/login")
@ApiOperation("登录")
public ResponseVO login(@RequestBody LoginDTO loginDTO) {
return ResponseVO.success(userService.login(loginDTO));
}
@PostMapping("/echo")
@ApiOperation("测试")
@RequiresRoles(value = {"admin"})
public ResponseVO echo() {
return ResponseVO.success("SUCCESS");
}
}
启动类
需要加上 Mybatis 的注解 MapperScan 进行 Mapper 的扫描
@SpringBootApplication
@MapperScan(basePackages = {"com.jiangchunbo.dao"})
public class StartEntry {
public static void main(String[] args) {
SpringApplication.run(StartEntry.class, args);
}
}
测试
用户名不存在
密码错误
登录成功
登录成功之后返回 token