SpringBoot 使用SpringSecurity 前后端分离
文章中使用SpringBoot 和 SpringSecurity ,使用JWT 用来做认证,版本选择是 2.1.0
选择mybatis 用来做 持久化,mysql 8.0。
创建用户角色权限数据库
用户数据库
DROP TABLE IF EXISTS `stack_admin`;
CREATE TABLE `stack_admin` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`account` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`code` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`locked` int(2) NOT NULL DEFAULT 0,
`last_login_time` bigint(50) NOT NULL,
`last_password_reset` bigint(50) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `admin_name`(`username`) USING BTREE,
INDEX `admin_account`(`account`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1153213503822143490 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of stack_admin
-- ----------------------------
INSERT INTO `stack_admin` VALUES (1153213503822143490, 'gssznb', '$2a$10$dQBQGQRkpxsaPgXWfTTWyuCZ66Pno3a8j9fRzBd/HJEh.0FE5dxzK', 'gssznb', '1181954449@qq.com', '15152268067', '江苏省', 'gss', 'xxxx', 1, 1563782499462, 1563782499462);
创建ROLE 表格
-- ----------------------------
-- Table structure for stack_role
-- ----------------------------
DROP TABLE IF EXISTS `stack_role`;
CREATE TABLE `stack_role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `role_id`(`id`) USING BTREE,
INDEX `role_name`(`role_name`) USING BTREE,
INDEX `in_role_code`(`role_code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of stack_role
-- ----------------------------
INSERT INTO `stack_role` VALUES (1, '系统管理员', 'ROLE_system');
INSERT INTO `stack_role` VALUES (2, '用户管理', 'ROLE_user');
SET FOREIGN_KEY_CHECKS = 1;
创建关联表格admin_role
-- ----------------------------
-- Table structure for admin_role
-- ----------------------------
DROP TABLE IF EXISTS `admin_role`;
CREATE TABLE `admin_role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`admin_id` bigint(11) NOT NULL,
`role_id` bigint(11) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `in_admin_id`(`admin_id`) USING BTREE,
INDEX `in_role_id`(`role_id`) USING BTREE,
CONSTRAINT `a_r_admin_id` FOREIGN KEY (`admin_id`) REFERENCES `stack_admin` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `a_r_role_id` FOREIGN KEY (`role_id`) REFERENCES `stack_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of admin_role
-- ----------------------------
INSERT INTO `admin_role` VALUES (1, 1153213503822143490, 1);
创建权限表 permission
-- ----------------------------
-- Table structure for stack_permission
-- ----------------------------
DROP TABLE IF EXISTS `stack_permission`;
CREATE TABLE `stack_permission` (
`id` bigint(11) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`type` int(2) NOT NULL,
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`order` bigint(20) NOT NULL,
`p_id` bigint(11) NOT NULL,
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `in_per_name`(`name`) USING BTREE,
INDEX `in_per_type`(`type`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of stack_permission
-- ----------------------------
INSERT INTO `stack_permission` VALUES (1, '删除用户', 'd', 1, 'admin/d', 1, 0, '无');
INSERT INTO `stack_permission` VALUES (2, '添加用户', 'c', 1, 'admin/c', 1, 0, '无');
INSERT INTO `stack_permission` VALUES (3, '读取用户', 'r', 1, 'admin/r', 1, 0, '无');
创建权限角色关联表格role_permission
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`id` bigint(11) NOT NULL,
`role_id` bigint(11) NOT NULL,
`permission_id` bigint(11) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `r_p_role_id`(`role_id`) USING BTREE,
INDEX `r_p_per_id`(`permission_id`) USING BTREE,
CONSTRAINT `r_p_per_id` FOREIGN KEY (`permission_id`) REFERENCES `stack_permission` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `r_p_role_id` FOREIGN KEY (`role_id`) REFERENCES `stack_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES (1, 1, 1);
INSERT INTO `role_permission` VALUES (2, 1, 2);
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>memory</artifactId>
<groupId>cn.fllday</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>memory-web</artifactId>
<dependencies>
<dependency>
<groupId>cn.fllday</groupId>
<artifactId>memory-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--jwt -->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
</project>
创建model
本文选用 lombok,mybatis-plus .代码生成器,生成实体类,先贴出来生成代码把!
package cn.fllday.generator;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* @Author:gssznb
* @Date:Created 2019/7/9 14:37
*/
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (ipt!=null && ipt.length() != 0) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
System.out.println(projectPath);
gc.setOutputDir("C:\\Users\\MyPC\\Desktop\\memory\\memory-api\\src\\main\\java");
gc.setAuthor("gssznb");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/stack?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("Root_12root");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(scanner("模块名"));
pc.setParent("cn.fllday");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
// String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<FileOutConfig>();
// 自定义配置会被优先输出
// focList.add(new FileOutConfig(templatePath) {
// @Override
// public String outputFile(TableInfo tableInfo) {
// // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
// return "Z:\\gssznb\\workspace\\spring-security-learn\\stack-api\\src\\main\\resource\\mapper/" + pc.getModuleName()
// + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
// }
// });
/*
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判断自定义文件夹是否需要创建
checkDir("调用默认方法创建的目录");
return false;
}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// // 配置模板
// TemplateConfig templateConfig = new TemplateConfig();
//
// // 配置自定义输出模板
// //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// // templateConfig.setEntity("templates/entity2.java");
// // templateConfig.setService();
// // templateConfig.setController();
//
// templateConfig.setXml(null);
// mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// strategy.setSuperEntityClass("cn.fllday.stack.common.BaseEntity");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
// strategy.setSuperControllerClass("cn.fllday.stack.common.BaseController");
// 写于父类中的公共字段
strategy.setSuperEntityColumns("id");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
// mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
实体类
StackAdmin
package cn.fllday.memory.entity;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @author gssznb
* @since 2019-07-22
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class StackAdmin implements Serializable {
private static final long serialVersionUID=1L;
private Long id;
private String username;
private String password;
private String account;
private String email;
private String phone;
private String address;
private String name;
private String code;
private Integer locked;
private Long lastLoginTime;
private Long lastPasswordReset;
}
StackPermission
package cn.fllday.memory.entity;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
*
* </p>
*
* @author gssznb
* @since 2019-07-23
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class StackPermission implements Serializable {
private static final long serialVersionUID=1L;
private String name;
private String code;
private Integer type;
private String url;
private Long order;
private Long pId;
private String description;
}
StackRole
package cn.fllday.memory.entity;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @author gssznb
* @since 2019-07-22
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class StackRole implements Serializable {
private static final long serialVersionUID=1L;
private Long id;
private String roleName;
private String roleCode;
}
Dao层
StackAdminMapper
package cn.fllday.memory.mapper;
import cn.fllday.memory.entity.StackAdmin;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
/**
* @author gssznb
* @since 2019-07-22
*/
@Mapper
@Repository
public interface StackAdminMapper extends BaseMapper<StackAdmin> {
@Select("select * from stack_admin where username = #{0}")
StackAdmin selectAdminByUsername(String username);
}
StackPermissionMapper
package cn.fllday.memory.mapper;
import cn.fllday.memory.entity.StackPermission;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author gssznb
* @since 2019-07-23
*/
@Mapper
public interface StackPermissionMapper extends BaseMapper<StackPermission> {
@Select("SELECT * FROM stack_permission AS p LEFT JOIN role_permission AS rp ON p.id = rp.permission_id LEFT JOIN stack_role AS r on r.id = rp.role_id WHERE role_code = #{0}")
List<StackPermission> selectPermissionByRoleCode(String code);
}
StackRoleMapper
package cn.fllday.memory.mapper;
import cn.fllday.memory.entity.StackRole;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author gssznb
* @since 2019-07-22
*/
@Mapper
@Repository
public interface StackRoleMapper extends BaseMapper<StackRole> {
@Select("select * from stack_role as r left join admin_role as ar on r.id = ar.role_id left join stack_admin as a on a.id = ar.admin_id where a.id = #{0} ")
List<StackRole> selectRoleByUserId(Long id);
}
Service 层,代码不多,而且一个接口对应一个实现类。就放在一块了
Admin 的 Service
public interface IStackAdminService extends IService<StackAdmin> {
StackAdmin findAdminByUsername(String username);
boolean addAdmin(String username,String password);
String findAdminByUsernamePassword(String username,String password);
String refreshToken(String token);
}
// 实现类
@Service
public class StackAdminServiceImpl extends ServiceImpl<StackAdminMapper, StackAdmin> implements IStackAdminService {
@Override
public StackAdmin findAdminByUsername(String username) {
return stackAdminMapper.selectAdminByUsername(username);
}
@Override
public boolean addAdmin(String username, String password) {
StackAdmin stackAdmin = new StackAdmin();
stackAdmin.setName("gss").setLastPasswordReset(System.currentTimeMillis()).setUsername(username).setAccount(username).setAddress("江苏省").setCode("xxxx").setEmail("1181954449@qq.com").setLastLoginTime(System.currentTimeMillis()).setPhone("15152268067").setLocked(1).setPassword(getPassword(password));
int result = stackAdminMapper.insert(stackAdmin);
return result == 1;
}
/**
* 用户名密码
* @param username
* @param password
* @return
*/
@Override
public String findAdminByUsernamePassword(String username, String password) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authenticate);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String token = jwtTokenUtils.generateToken(userDetails);
return token;
}
@Override
public String refreshToken(String oldToken) {
String token = oldToken.substring(tokenHead.length());
String username = jwtTokenUtils.getUsernameFromToken(token);
CustomUserDetils custom = (CustomUserDetils) userDetailsService.loadUserByUsername(username);
if (jwtTokenUtils.canTokenBeRefreshed(token,new Date(custom.getStackAdmin().getLastPasswordReset()))){
return jwtTokenUtils.refreshToken(token);
}
return null;
}
private String getPassword(String pwd){
return new BCryptPasswordEncoder().encode(pwd);
}
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private StackAdminMapper stackAdminMapper;
private AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
private JwtTokenUtils jwtTokenUtils;
@Autowired
public StackAdminServiceImpl(
AuthenticationManager authenticationManager,
UserDetailsService userDetailsService,
JwtTokenUtils jwtTokenUtils) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtTokenUtils = jwtTokenUtils;
}
}
这里先不要管这个token 什么的东西。我们先把spring security 整合好
Permission的Service
public interface IStackPermissionService extends IService<StackPermission> {
List<StackPermission> findPermissionByRoleCode(String code);
}
@Service
public class StackPermissionServiceImpl extends ServiceImpl<StackPermissionMapper, StackPermission> implements IStackPermissionService {
@Autowired
private StackPermissionMapper stackPermissionMapper;
public List<StackPermission> findPermissionByRoleCode(String code){
return stackPermissionMapper.selectPermissionByRoleCode(code);
}
}
Role的Service
// 接口
public interface IStackRoleService extends IService<StackRole> {
List<StackRole> findRoleByUserId(Long id);
}
// 实现类
@Service
public class StackRoleServiceImpl extends ServiceImpl<StackRoleMapper, StackRole> implements IStackRoleService {
@Autowired
private StackRoleMapper stackRoleMapper;
@Override
public List<StackRole> findRoleByUserId(Long id) {
return stackRoleMapper.selectRoleByUserId(id);
}
}
配置SpringSecurity的配置类
package cn.fllday.memory.config.security;
import cn.fllday.memory.config.jwt.JwtAuthenticationEntryPoint;
import cn.fllday.memory.config.jwt.JwtAuthenticationTokenFilter;
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.authentication.AuthenticationManager;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private UserDetailsService UserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(UserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
// .antMatchers("auth/**")
// .permitAll()
// .anyRequest().authenticated();
.formLogin().loginPage("/login")
// .loginProcessingUrl("/login.do")
// failureUrl() 指定认证失败后Url,defaultSuccessUrl()
// 指定认证成功后Url。我们可以通过设置 successHandler() 和 failureHandler()
// 来实现自定义认证成功、失败处理。
// 当我们设置了这两个后,需要去除 failureUrl() 和 defaultSuccessUrl() 的设置
// ,否则无法生效。这两套配置同时只能存在一套。
.defaultSuccessUrl("/login/success")
.failureUrl("/login/error").permitAll()
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout().permitAll();
// http.csrf().disable().exceptionHandling().accessDeniedPage("/stack/authorication");
// http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
http
// 由于使用的是JWT,我们这里不需要csrf
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
//.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允许对于网站静态资源的无授权访问
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
// 对于获取token的rest api要允许匿名访问
.antMatchers("/auth/**")
.permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 添加JWT filter
http
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
// 禁用缓存
http.headers().cacheControl();
}
/**
* 设置静态资源不被拦截
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**","/js/**");
}
@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler(){
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(new CustomPermissionEvaluator());
return handler;
}
// 注入刚刚的jwtFilter
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
UserDetailsService UserDetailsService;
这个类是用来根据用户名查询该用户的信息。 这个类需要实现SpringSecurity 中的给我们指定的一个接口 UserDetailsService
. 自定义实现这个方法
package cn.fllday.memory.config.security;
import cn.fllday.memory.config.model.CustomRole;
import cn.fllday.memory.config.model.CustomUserDetils;
import cn.fllday.memory.entity.StackAdmin;
import cn.fllday.memory.entity.StackRole;
import cn.fllday.memory.service.IStackAdminService;
import cn.fllday.memory.service.IStackRoleService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
@Slf4j
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private IStackAdminService iStackAdminService;
@Autowired
private IStackRoleService iStackRoleService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 从数据库中取出数据信息
if (StringUtils.isEmpty(s)){
log.info("用户名输入不能为空");
throw new UsernameNotFoundException("用户名不能为空");
}
StackAdmin admin = iStackAdminService.findAdminByUsername(s);
if (admin == null){
throw new UsernameNotFoundException("该用户名不存在!");
}
List<StackRole> roles = iStackRoleService.findRoleByUserId(admin.getId());
log.info("用户:{}开始查询对应的角色." , s);
for (StackRole r: roles ) {
authorities.add(new CustomRole(r));
}
return new CustomUserDetils(admin,new HashSet<>(authorities));
}
}
这个CustomUserDetailsService
的loadUserByUsername()
方法是用来根据用户名查询用户信息的。 返回一个UserDetails
,userDetails
是SpringSecurity 提供给我们的一个用户类。我们这边自定义一个并且实现这个类
GrantedAuthority
也是SpringSecurity 提供我们的一个类。
CustomUserDetils 类
package cn.fllday.memory.config.model;
import cn.fllday.memory.entity.StackAdmin;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;
@Data
public class CustomUserDetils implements UserDetails {
private StackAdmin stackAdmin;
private Set<GrantedAuthority> authorities;
public CustomUserDetils(StackAdmin stackAdmin, Set<GrantedAuthority> authorities) {
this.stackAdmin = stackAdmin;
this.authorities = authorities;
}
// 获取角色的集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
// 获取密码
@Override
public String getPassword() {
return stackAdmin.getPassword();
}
// 获取用户名
@Override
public String getUsername() {
return stackAdmin.getUsername();
}
// 帐号是否过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 帐号是否锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
// 帐号密码是否过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
CustomRole
package cn.fllday.memory.config.model;
import cn.fllday.memory.entity.StackRole;
import org.springframework.security.core.GrantedAuthority;
public class CustomRole implements GrantedAuthority {
private StackRole stackRole;
public CustomRole(StackRole stackRole) {
this.stackRole = stackRole;
}
@Override
public String getAuthority() {
return this.stackRole.getRoleCode();
}
}
getAuthority
这个方法返回的是 你的角色代码 就类似于这种ROLE_user
整合JWT
application.yml 配置
server:
port: 8080
spring:
application:
name: memory
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/stack?useUnicode=true&Encoding=utf8&charaterSetResults=utf8&serverTimezone=GMT%2B8
username: root
password: Root_12root
redis:
host: 192.168.157.129
main:
allow-bean-definition-overriding: true
freemarker:
suffix: .ftl
template-loader-path: classpath:/templates
jwt:
header: Authorization
secret: mySecret
expiration: 604800
tokenHead: "Bearer "
route:
authentication:
path: auth
refresh: refresh
register: "auth/register"
JWT的工具类
JwtUtils
package cn.fllday.util;
import cn.fllday.memory.config.model.CustomUserDetils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtTokenUtils implements Serializable {
private static final long serialVersionUID = -3301605591108950415L;
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;
public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
public Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
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);
}
String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getCreatedDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& !isTokenExpired(token);
}
public String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
public Boolean validateToken(String token, UserDetails userDetails) {
CustomUserDetils user = (CustomUserDetils) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getCreatedDateFromToken(token);
//final Date expiration = getExpirationDateFromToken(token);
return (
username.equals(user.getUsername())
&& !isTokenExpired(token)
&& !isCreatedBeforeLastPasswordReset(created, new Date(user.getStackAdmin().getLastPasswordReset())));
}
}
JwtAuthenticationTokenFilter类
package cn.fllday.memory.config.jwt;
import cn.fllday.util.JwtTokenUtils;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtils jwtTokenUtils;
@Value("${jwt.header}")
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(tokenHead)){
final String authToken = authHeader.substring(tokenHead.length());
String username = jwtTokenUtils.getUsernameFromToken(authToken);
log.info("用户{}请求",username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtils.validateToken(authToken,user)){
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request,response);
}
}
JwtAuthenticationEntryPoint的类
package cn.fllday.memory.config.jwt;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
创建Controller 层进行测试
StackAdminController
package cn.fllday.memory.controller;
import cn.fllday.memory.service.IStackAdminService;
import cn.fllday.util.ResponseData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* </p>
*
* @author gssznb
* @since 2019-07-22
*/
@RestController
public class StackAdminController {
@Autowired
private IStackAdminService iStackAdminService;
@GetMapping(value = "test")
public String test(){
return "test";
}
@PostMapping(value = "add.do")
public ResponseData add(String username,String password){
boolean result = iStackAdminService.addAdmin(username, password);
return result?ResponseData.ok():ResponseData.serverInternalError();
}
@RequestMapping(value = "admin/sys")
@PreAuthorize("hasRole('ROLE_system')")
public String system(){
return "如果你能看到这句话,说明你有system角色";
}
@RequestMapping(value = "admin/system")
@PreAuthorize("hasRole('ROLE_user')")
public String user(){
return "如果你能看到这句话,说明你有user角色";
}
@RequestMapping(value = "admin/r")
@PreAuthorize("hasPermission('admin/r','r')")
public String adminR(){
return "说明你有admin角色r路径权限";
}
@RequestMapping(value = "admin/d")
@PreAuthorize("hasPermission('admin/d','d')")
public String adminD(){
return "说明你有admin角色d路径权限";
}
@RequestMapping(value = "admin/c")
@PreAuthorize("hasPermission('admin/c','c')")
public String adminC(){
return "说明你有admin角色c路径权限";
}
}
StackAdminController 测试权限和角色是否可用
CustomSystemController
package cn.fllday.memory.controller;
import cn.fllday.memory.config.jwt.JwtAuthenticationRequest;
import cn.fllday.memory.service.IStackAdminService;
import cn.fllday.util.ResponseData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
public class CustomSystemController {
@Autowired
private IStackAdminService iStackAdminService;
@RequestMapping(value = "/login/error")
@ResponseBody
public ResponseData error() {
return ResponseData.userError();
}
@GetMapping(value = "login")
public String login() {
return "login";
}
@GetMapping(value = "system/register")
@ResponseBody
public boolean register(String username,String password){
return iStackAdminService.addAdmin(username,password);
}
@GetMapping(value = "/login/success")
public String success() {
return "home";
}
@GetMapping(value = "/stack/authorication")
public String authorication(){
return "authorication";
}
@RequestMapping(value = "${jwt.route.authentication.path}",method = RequestMethod.POST)
@ResponseBody
public ResponseData loginToken(@RequestBody JwtAuthenticationRequest request){
String password = request.getPassword();
String username = request.getUsername();
String token = iStackAdminService.findAdminByUsernamePassword(username, password);
return token!=null?ResponseData.ok().putDataValue("token",token):ResponseData.badRequest().putDataValue("token",token);
}
}
使用POSTMAN 进行测试
使用postman访问 /auth这个接口 进行登录
查看我们后台的日志打印了什么
访问需要用户权限的路径
http://localhost:8080/admin/d
访问的时候需要携带你登录时,返回给你的token。
后台所做的查询操作
这边我们是如果用户携带了token ,那么就会将该用户的用户名再去数据库中查询一次。这个查询也可以不做。 查询成功之后,就再去查询 权限。
如果没有携带token的话,访问。使用postman测试
可以看到返回 401 错误。没有通过认证。