自定义资源权限规则
在项目中添加如下配置就可以实现对资源权限规则设定:
创建一个配置类,继承WebSecurityConfigurerAdapter,重写其中的configure(HttpSecurity http)方法,最终在类上使用@Configuration.
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/index")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
}
# 说明
- permitAll() 代表放行该资源,该资源为公共资源,无需认证和授权可以直接访问
- anyRequest().authenticated()代表所有请求,必须认证之后才能访问
- formLogin() 代表开启表单认证
## 注意:放行资源必须放在所有认证请求之前!
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/loginHtml").permitAll() //请求路径,即Controller路径,放行自定义登录界面
.mvcMatchers("/index").permitAll() //请求路径/index放行
.anyRequest().authenticated() //其他的任何请求都要进行认证
.and()
.formLogin() //表单验证
.loginPage("/loginHtml") //默认登录页面,也是通过请求进行跳转至默认登录页面
.loginProcessingUrl("/doLogin") //指定发送过来的/doLogin请求被捕获,进行权限验证。
.usernameParameter("uname") //默认接收username参数,修改为uname
.passwordParameter("passwd") //默认接收password参数,修改为passwd
// .successForwardUrl("/index") //认证成功 forward跳转路径 地址栏不变 每次都默认跳转到/index
.defaultSuccessUrl("/index",true) //认证成功之后的跳转 重定向
// 如果之前保存了请求但是被拦截,在拦截之后先跳转到被保存的请求,如果没有,则跳转到指定的请求。
// 如果第二个参数设置为true,则无论如何,只要认证成功,不管有没有保存的请求直接跳转到指定的请求。
.and()
.csrf().disable(); //禁止 csrf 跨域请求保护
}
}
需要注意的是
登录表单method必须为post。
antion的请求路径与配置类中的loginProcessingUrl()一致。
用户名密码的参数也需要与配置类中的usernameParameter()、passwordParameter()一致。
successForwardUrl、defaultSuccessUrl这个两个方法都可以实现成功之后跳转
有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面。只需要给前端返回一个JSON通知登录成功还是失败与否。这个时候可以通过自定义AuthenticationSuccessHandler实现。
public interface AuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the Authentication object which was created during
* the authentication process.
*/
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException;
}
根据接口的描述信息,也可以得知登录成功会自动回调这个方法,进一步查看它的默认类型,发现successForwardUrl、defaultSuccessUrl也是由它的子类实现的。
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String,Object> result = new HashMap<String,Object>();
result.put("msg","登录成功");
result.put("status",200);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
//...
.and()
.formLogin() //表单验证
//...
//自定义登录成功处理
.successHandler(new MyAuthenticationSuccessHandler())
.failureForwardUrl("/loginHtml")
.and()
.csrf().disable(); //禁止 csrf 跨域请求保护
}
}
为了能够更直观在登录页面看到异常错误信息,可以在登录页面中直接获取异常信息。Spring Security在登录失败之后会将异常信息存储到request,session作用域中key为SPRING_SECURITY_LAST_EXCEPTION命名属性中。
源码可以参考:SimpleUrlAuthenticationFailureHandler
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
//...
.and()
.formLogin() //表单验证
//...
//.failureForwardUrl("/loginHtml") //认证失败之后的forward跳转 forward -->异常信息存放在request
.failureUrl("/loginHtml") // 默认 认证失败之后的 redirect 跳转 redirect -->异常信息存放在session
.and()
.csrf().disable(); //禁止 csrf 跨域请求保护
}
}
和自定义登录成功处理一样,Spring Security同样为前后端分离开发提供了登录失败的处理,这个类就是AuthenticationFailureHandler。
源码:
public interface AuthenticationFailureHandler {
/**
* Called when an authentication attempt fails.
* @param request the request during which the authentication attempt occurred.
* @param response the response.
* @param exception the exception which was thrown to reject the authentication
* request.
*/
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException;
}
根据接口的描述信息,也可以得知登录失败会自动回调这个方法,进一步查看它的默认实现,发现failureUrl、failureForwardUrl也是由它的子类实现的。
/*
* 自定义登录失败解决方案
* */
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map<String,Object> result = new HashMap<>();
result.put("msg","登录失败:"+exception.getMessage());
result.put("status",500);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
配置AuthenticationFailureHandler
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
//...
.and()
.formLogin() //表单验证
//...
.successHandler(new MyAuthenticationSuccessHandler()) //用来自定义认证成功之后处理 前后端分离解决方案
// .failureForwardUrl("/loginHtml") //认证失败之后的forward跳转 forward -->异常信息存放在request
// .failureUrl("/loginHtml") // 默认 认证失败之后的 redirect 跳转 redirect -->异常信息存放在session
.failureHandler(new MyAuthenticationFailureHandler()) //用来自定义认证失败之后处理 前后端分离解决方案
.and()
.csrf().disable(); //禁止 csrf 跨域请求保护
}
}
Spring Security中提供了默认的注销登录配置,在开发时也可以按照自己需求对注销进行个性化定制。
开启注销登录 默认开启
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
//...
.and()
.formLogin() //表单验证
//...
.and()
.logout() //开启注销登录 默认
.logoutUrl("/logout") //指定注销登录 url 默认 默认请求方式:GET
.invalidateHttpSession(true) //默认 会话失效
.clearAuthentication(true) //默认 清楚认证标记
.logoutSuccessUrl("/loginHtml") //注销登录成功之后跳转
.and()
.csrf().disable(); //禁止 csrf 跨域请求保护
}
}
配置多个注销登录请求
如果项目中有需要,开发者还可以配置多个注销登录的请求,同时还可以指定请求的方法:
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
//...
.and()
.formLogin() //表单验证
//...
.and()
.logout() //开启注销登录 默认
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/aa","GET"),
new AntPathRequestMatcher("/bb","POST")
)) //
.invalidateHttpSession(true) //默认 会话失效
.clearAuthentication(true) //默认 清楚认证标记
.logoutSuccessUrl("/loginHtml") //注销登录成功之后跳转
.and()
.csrf().disable(); //禁止 csrf 跨域请求保护
}
}
前后端分离注销登录配置
如果是前后端分离开发,注销成功之后就不需要页面跳转了,只需要将注销成功的信息返回前端即可,此时我们可以通过自定义LogoutSuccessHandler实现来返回内容注销之后信息:
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String,Object> result = new HashMap<String,Object>();
result.put("msg","注销成功");
result.put("status",200);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
配置
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
//...
.and()
.formLogin() //表单验证
//...
.and()
.logout() //开启注销登录 默认
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/aa","GET"),
new AntPathRequestMatcher("/bb","POST")
)) //
.invalidateHttpSession(true) //默认 会话失效
.clearAuthentication(true) //默认 清楚认证标记
.logoutSuccessHandler(new MyLogoutSuccessHandler()) //注销登录成功之后处理
.and()
.csrf().disable(); //禁止 csrf 跨域请求保护
}
}
Spring Security会将登录用户数据保存在Session中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security会将登录成功的用户信息保存到SecurityContextHolder中。
SecurityContextHolder中的数据保存默认是通过ThreadLocal来实现的,使用ThreadLocal创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContextHolder中的数据清空。以后每当有请求到来时,Spring Security就会先从Session中取出用户登录数据,保存到SecurityContextHolder中,方便在该请求的后续处理过程中使用,同时在请求结束时将SecurityContextHolder中的数据拿出来保存到Session中,然后将SecurityContextHolder中的数据清空。
实际那个SecurityContextHolder中存储的是SecurityContext,在SecurityContext中存储是Authentication。
这种设计是典型的策略设计模式:
public class SecurityContextHolder {
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
//...
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
//...
}
通过SecurityContextHolder可以得知,SecurityContextHolderStrategy接口用来定义存储策略方法
public interface SecurityContextHolderStrategy {
void clearContext();
SecurityContext getContext();
void setContext(SecurityContext context);
SecurityContext createEmptyContext();
}
接口中一共定义了四个方法:
从上面可以看出每一个实现类对应一种策略的实现。
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();
System.out.println("身份信息:"+user.getUsername());
System.out.println("权限信息:"+authentication.getAuthorities());
new Thread(()->{
Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();
System.out.println("子线程:"+authentication1);
}).start();
System.out.println("hello security");
return "hello spring security";
}
}
从源码中可以知道,该策略可以从系统参数中获取,因此覆盖系统中的SYSTEM_PROPERTY参数即可。
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
因此在启动项目是设置系统参数即可。
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
<version>3.0.4.RELEASEversion>
dependency>
<html lang="en" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
AuthenticationManager是认真的核心类,但实际上在底层真正认证时还离不开ProviderManager以及AuthenticationProvider。他们三者关系是怎么样的呢?
AuthenticationManager与ProviderManager
ProviderManager是AuthenticationManager的唯一实现,也是Spring Security默认使用实现。从这里不难看出默认情况下
AuthenticationManager就是一个ProviderManager。
ProviderManager与AuthenticationProvider
https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html
在Spring Security中,允许系统同时支持多种不同的认证方式,例如同时支持用户名/密码认证、RememberMe认证、手机号码动态认证等,而不同的认证方式对应了不同的AuthenticationProvider,所以一个完整的认证流程可能由多个AuthenticationProvider来提供。
多个AuthenticationProvider将组成一个列表,这个列表将由ProviderManager代理。换句话说,在ProviderManager中存在一个AuthenticationProvider列表,在ProviderManager中遍历列表中的每一个AuthenticationProvider去执行身份认证,最终得到认证结果。
ProviderManager本身也可以再配置一个AuthenticationManager作为parent,这样当ProviderManager认证失败之后,就可以进入到parent中再次进行认证。理论上来说,ProviderManager的parent可以是任意类型的AuthenticationManager,但是通常都是由ProviderManager来扮演parent的角色,也就是ProviderManager是ProviderManager的parent。
ProviderManager本身也可以有多个,多个ProviderManager共用同一个parent。有时,一个应用程序有受保护资源的逻辑组(例如,所有符合路径模式的网络资源,如/api/**),每个组可以有自己的专用AuthenticationManager。通常,每个组都是一个ProviderManager,它们共享一个父级,然后,父级是一种全局资源,作为所有提供者的后备资源。
弄清楚认证原理之后我们来看下具体认证时数据源的获取。默认情况下,AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过UserDetailsService完成数据源的校验。他们之间调用关系如下:
总结:AuthenticationManager是认证管理器,在Spring Security中有全局AuthenticationManager,也可以有AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由ProviderManager进行实现。每一个ProviderManager中都代理一个AuthenticationProvier的列表,列表中每一个实现代表一种身份的认证方式。认证时底层数据源需要调用UserDetailService来实现。
https://spring.io/guides/topical/spring-security-architecture
//方法名字自定义
@Autowired
public void initialize(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("springboot 默认配置:" + builder);
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("zkt").password("{noop}123").roles("admin").build());
builder.userDetailsService(userDetailsService);
}
//方法名字自定义(上述为一个示例)
@Autowired
public void initialize(AuthenticationManagerBuilder builder) throws Exception {
//builder...
}
总结:1.默认自动配置全局AuthenticationManager 默认找当前项目中是否存在自定义UserDetailService实例,自动将当前项目UserDetailService实例设置为数据源
2.默认自动配置创建全局AuthenticationManager在工厂中使用时直接在代码中注入即可。
Springboot对security进行自动配置时自动在工厂中创建一个全局AuthenticationManager。
// 自定义AuthenticationManager
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义AuthenticationManager:" + builder);
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("zkt").password("{noop}root").roles("admin").build());
builder.userDetailsService(userDetailsService);
}
// 自定义AuthenticationManager(上述为一个实例)
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
//...
}
总结:1.一旦通过configure方法自定义AuthenticationManager实现 就会将工厂中自动配置Authentication进行覆盖
2.一旦通过configure方法自定义AuthenticationManager实现 需要在实现中指定认证数据源对象UserDetailService实例
3.一旦通过configure方法自定义AuthenticationManager实现 这种方式创建AuthenticationManager对象,工厂内部创建一个本地AuthenticationManager对象,不允许在其他自定义组件中进行注入。
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("zkt").password("{noop}123").roles("admin").build());
return userDetailsService;
}
// 自定义AuthenticationManager 并没有在工厂中暴露出来
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义AuthenticationManager:" + builder);
builder.userDetailsService(userDetailsService());
}
//作用:用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
设计表结构
-- 用户表
CREATE TABLE `user`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enable` tinyint(1) DEFAULT NULL,
`accountNonExpired` tinyint(1) DEFAULT NULL,
`accountNonLocked` tinyint(1) DEFAULT NULL,
`credentialsNonExpired` tinyint(1) DEFAULT NULL,
PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 角色表
CREATE TABLE `role`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`name_zh` varchar(32) DEFAULT NULL,
PRIMARY KEY(`id`)
)ENGINE=InnoDB Auto_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 用户角色关系表
CREATE TABLE `user_role`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY(`id`),
KEY `uid`(`uid`),
KEY `rid`(`rid`)
)ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
插入测试数据
-- 插入用户数据
BEGIN;
INSERT INTO `user`
values(1,'root','{noop}123',1,1,1,1);
INSERT INTO `user`
values(2,'admin','{noop}123',1,1,1,1);
INSERT INTO `user`
values(3,'blr','{noop}123',1,1,1,1);
COMMIT;
-- 插入角色数据
BEGIN;
INSERT INTO `role`
values(1,'ROLE_product','商品管理员');
INSERT INTO `role`
values(2,'ROLE_admin','系统管理员');
INSERT INTO `role`
values(3,'ROLE_user','用户管理员');
COMMIT;
-- 插入用户角色数据
BEGIN
INSERT INTO `user_role`
values(1,1,1);
INSERT INTO `user_role`
values(2,1,2);
INSERT INTO `user_role`
values(3,2,2);
INSERT INTO `user_role`
values(4,3,3);
COMMIT;
引入依赖
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.38version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
配置springboot配置文件
#设置thymeleaf 缓存
spring.thymeleaf.cache=false
#配置数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#mybatis配置
#注意mapper目录必须使用"/"
mybatis.mapper-locations=classpath:com/zkt/mapper/*.xml
mybatis.type-aliases-package=com.zkt.entity
#日志处理 为了展示mysql运行 sql语句
logging.level.com.zkt=debug
创建entity
user对象
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled; //账户是否激活
private Boolean accountNonExpired; //账号是否过期
private Boolean accountNonLocked; //账户是否被锁定
private Boolean credentialsNonExpired; //密码是否过期
private List<Role> roles = new ArrayList<>(); //关系属性 用来存储当前用户所有角色信息
//返回权限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role->{
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public void setAccountNonExpired(Boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(Boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public Integer getId() {
return id;
}
public List<Role> getRoles() {
return roles;
}
}
role对象
public class Role {
private Integer id;
private String name;
private String nameZh;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNameZh() {
return nameZh;
}
public void setNameZh(String nameZh) {
this.nameZh = nameZh;
}
}
编写Dao类
@Mapper
public interface UserDao {
//提供根据用户名返回用户方法
User loadUserByUsername(String username);
//提供根据用户id查询用户角色信息方法
List<Role> getRolesByUid(Integer uid);
}
mapper
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zkt.dao.UserDao">
<select id="loadUserByUsername" resultType="User">
select id,username,password,enabled,accountNonExpired,accountNonLocked,credentialsNonExpired
from user
where username = #{username}
select>
<select id="getRolesByUid" resultType="Role">
select r.id,r.name,r.name_zh from role r, user_role ur
where r.id = ur.rid
and ur.rid = #{uid}
select>
mapper>
自定义UserDetailsService
@Component
public class MyUserDetailService implements UserDetailsService {
private final UserDao userDao;
//dao ==>springboot+mybatis
@Autowired
public MyUserDetailService(UserDao userDao){
this.userDao = userDao;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.查询用户
User user = userDao.loadUserByUsername(username);
if(ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确~");
// 2.查询权限信息
List<Role> roles = userDao.getRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
}
配置authenticationManager使用自定义UserDetailService
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final MyUserDetailService myUserDetailService;
@Autowired
public WebSecurityConfigurer(MyUserDetailService myUserDetailService) {
this.myUserDetailService = myUserDetailService;
}
// 自定义AuthenticationManager 并没有在工厂中暴露出来
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义AuthenticationManager:" + builder);
// InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
// userDetailsService.createUser(User.withUsername("zkt").password("{noop}root").roles("admin").build());
builder.userDetailsService(myUserDetailService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
}
}
测试
使用账号 root 密码123进行登录 登录成功。