使用自定义的认证页面
xml配置
首先在SpringSecurity的配置文件中配置认证信息,包括释放静态资源,让认证页面可以被访问,配置认证信息以及退出设置
释放静态资源
<!-- 释放静态资源 -->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
让认证页面可以被访问
这个需要在一开始的<security:http>
标签中设置
<!-- 让认证页面可以被匿名访问 -->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
配置认证信息
也是在<security:http>
标签中设置
<!-- 配置认证信息 -->
<!--
login-page: 登录页面
login-processing-url:登录处理器
default-target-url:登录成功后跳转的地址
authentication-failure-url:登录失败跳转的地址
username-parameter: 登录表单的name属性值
-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"
username-parameter="username"
password-parameter="password"/>
退出设置
<!-- 配置退出设置 -->
<security:logout logout-url="/logout"
logout-success-url="/login.jsp"/>
最终配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--
auto-config="true": 表示自动加载spring-security的配置文件
use-expression="true": 表示使用spring的el表达式配置SpringSecurity
-->
<!-- 释放静态资源 -->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http auto-config="true" use-expressions="true">
<!-- 让认证页面可以被匿名访问 -->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!-- 拦截资源 -->
<!--
pattern="/**":表示拦截所有的资源
access="hasAnyRole('ROLE_USER')":表示只有ROLE_USER角色才能访问资源
-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
<!-- 配置认证信息 -->
<!--
login-page: 登录页面
login-processing-url:登录处理器
default-target-url:登录成功后跳转的地址
authentication-failure-url:登录失败跳转的地址
username-parameter: 登录表单的name属性值
-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"
username-parameter="username"
password-parameter="password"/>
<!-- 配置退出设置 -->
<security:logout logout-url="/logout"
logout-success-url="/login.jsp"/>
</security:http>
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<!--
创建了两个角色
springSecurity中的密码是加密的,如果想使用不加密的就要加上{noop}
-->
<security:user name="user" password="{noop}user" authorities="ROLE_USER"/>
<security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
修改请求页面的认证地址
配置完成后,需要将前端表单的请求地址设置为xml中配置的请求地址
csrf防护机制
来到登录页面,输入账户密码后点击登录,迎来的并不是登录成功的提示,而是403错误
解决这个错误的办法有两种,一种是禁用csrf过滤器
禁用csrf过滤器
同样在SpringSecurity中的<security:http>
中进行配置即可
<!-- 禁用csrf过滤器 -->
<security:csrf disabled="true"/>
在认证页面携带token请求
上面提到有两种办法解决,那么第二种就是携带csrf过滤器需要的认证token过去
至于为什么使用token就能够解决问题,这个答案可以在csrfFilter的源码中得到
public final class CsrfFilter extends OncePerRequestFilter {
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
private final Log logger = LogFactory.getLog(this.getClass());
private final CsrfTokenRepository tokenRepository;
private RequestMatcher requireCsrfProtectionMatcher;
private AccessDeniedHandler accessDeniedHandler;
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
this.accessDeniedHandler = new AccessDeniedHandlerImpl();
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository;
}
//通过这里可以看出SpringSecurity的csrf机制把请求方式分成两类来处理
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//第一类:"GET", "HEAD", "TRACE", "OPTIONS"四类请求可以直接通过
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
//第二类:除去上面四类,包括POST都要被验证携带token才能通过
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
}
} else {
filterChain.doFilter(request, response);
}
}
}
public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
this.accessDeniedHandler = accessDeniedHandler;
}
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet<String> allowedMethods;
private DefaultRequiresCsrfMatcher() {
this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
}
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}
}
看完源码就可以很清楚的知道,对于post请求,是一定要携带token的,那么携带token的方式也很简单,如下
只需要在form表单中添加<security:csrfInput/>
即可
登出配置
登出操作必须是post请求,因此可以使用下面的方式进行注销
<form action="${pageContext.request.contextPath}/logout" method="post">
<security:csrfInput/>
<input type="submit" value="注销">
</form>
初步实现认证功能
继承UserDetailsService接口
通过源码的阅读可知,SpringSecurity会使用UserDetailsService
类中的方法来加载用户信息,因此首先需要将我们自己的UserService
与UserDetailsService
关联起来,让我们自己的UserService
接口继承UserDetailsService
接口即可
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {
}
实现接口
然后实现其实现类,并在实现类中实现loadUserByUsername
方法
package com.itheima.service.impl;
import com.itheima.domain.SysRole;
import com.itheima.domain.SysUser;
import com.itheima.service.UserService;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional
public class UserServiceImpl implements UserService {
/**
* 写自己的获取用户的业务
*
* @param username 用户在浏览器输入的用户名
* @return UserDetails是SpringSecurity自己的用户对象
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
// 根据用户名去数据库做查询
// SysUser是我们自己的用户实体类对象
SysUser sysUser = userDao.findByName(username);
if (null == sysUser) {
// 没有该用户也是认证失败
return null;
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 添加角色
List<SysRole> roles = sysUser.getRoles();
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
// 构建SpringSecurity认可的用户对象 添加{noop}让SpringSecurity认为该密码就是原文
UserDetails userDetails = new User(sysUser.getUsername(), "{noop}" + sysUser.getPassword(), authorities);
return userDetails;
} catch (Exception e) {
e.printStackTrace();
// SpringSecurity内部认为return null 就是认证失败
return null;
}
}
}
在SpringSecurity主配置文件中指定认证使用的业务对象
这一步需要在springSecurity配置文件中将刚刚的实现类与之关联,由于实现类已经被放入了IOC容器中,因此写上id就行,这里没有id,类名首字母小写即可
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
</security:authentication-provider>
</security:authentication-manager>
最后刷新,再使用数据库的账户密码就可以进行登录了
加密认证
认证基本流程已经做完了,但是还有一些细节需要完善,例如现在的数据库以及认证时的密码都是原文的,这样显然是不安全的,因此后续需要进行加密认证
关于加密,SpringSecurity已经提供了加密对象BCryptPasswordEncoder
,接下来就是加密的步骤
在IOC容器中提供加密对象
<!-- 把加密对象放到ioc容器中 -->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
<!--指定认证使用的加密对象-->
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
修改认证方法
其实就是去掉之前自定义的loadUserByUsername
方法中密码前面的{noop}
修改添加用户的操作
完成上面的步骤就已经可以实现加密认证了,但是还有一个问题,那就是在添加用户的时候需要先将密码加密然后添加
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleService roleService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void save(SysUser user) {
//对密码进行加密,然后再入库
user.setPassword(passwordEncoder.encode(user.getPassword()));
userDao.save(user);
}
再次添加用户,密码就是加密后的密文了
设置用户状态
源码分析
用户认证业务里,在new User对象时,选择了三个构造参数的构造方法,其实还有另一个构造方法:
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (username != null && !"".equals(username) && password != null) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
可以看到,这个构造方法里多了四个布尔类型的构造参数,其实先前使用的三个构造参数的构造方法里这四个布尔
值默认都被赋值为了true,那么这四个布尔值到底是何意思呢?
- boolean enabled 是否可用
- boolean accountNonExpired 账户是否失效
- boolean credentialsNonExpired 秘密是否失效
- boolean accountNonLocked 账户是否锁定
判断认证用户的状态
这四个参数必须同时为true认证才可以,为了节省时间,只用第一个布尔值做个测试,修改认证业务代码:
/**
* 写自己的获取用户的业务
*
* @param username 用户在浏览器输入的用户名
* @return UserDetails是SpringSecurity自己的用户对象
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
// 根据用户名去数据库做查询
// SysUser是我们自己的用户实体类对象
SysUser sysUser = userDao.findByName(username);
if (null == sysUser) {
// 没有该用户也是认证失败
return null;
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 添加角色
List<SysRole> roles = sysUser.getRoles();
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
// 构建SpringSecurity认可的用户对象 添加{noop}让SpringSecurity认为该密码就是原文
UserDetails userDetails = new User(sysUser.getUsername(),
sysUser.getPassword(),
sysUser.getStatus() == 1,
true,
true,
true,
authorities);
return userDetails;
} catch (Exception e) {
e.printStackTrace();
// SpringSecurity内部认为return null 就是认证失败
return null;
}
}
此刻,只有用户状态为1的用户才能成功通过认证!
记住我功能实现
记住我功能页面代码
要实现记住我功能,首先需要前端准备一个单选框或者复选框,name为remember-me,value为true/on/yes/1等都可以
<input type="checkbox" name="remember-me" value="true">
开启remember me过滤器
<!-- 开启remember me过滤器 -->
<security:remember-me token-validity-seconds="60"/>
然后重启项目,就可以使用rememberMe功能了
说明:RememberMeAuthenticationFilter中功能非常简单,会在打开浏览器时,自动判断是否认证,如果没有则
调用autoLogin进行自动认证。
remember me安全性分析
记住我功能虽然很方便,但是问题在于它是将认证的token信息保存在浏览器的cookie中的,这样做是非常不安全的,因为别人可以轻易复制你的token信息去别的浏览器登录。
因此,SpringSecurity还提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个
无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录
时,用cookie中的加密串,到数据库中验证,如果通过,自动登录才算通过。
持久化remember me信息
创建一张表,注意这张表的名称和字段都是固定的,不要修改。
CREATE TABLE `persistent_logins` (
`username` VARCHAR(64) NOT NULL,
`series` VARCHAR(64) NOT NULL,
`token` VARCHAR(64) NOT NULL,
`last_used` TIMESTAMP NOT NULL, PRIMARY KEY (`series`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
然后修改配置文件中remember-me过滤器相关的配置
<!--
开启remember me过滤器,
data-source-ref="dataSource" 指定数据库连接池
token-validity-seconds="60" 设置token存储时间为60秒 可省略
remember-me-parameter="remember-me" 指定记住的参数名 可省略
-->
<security:remember-me data-source-ref="dataSource"
token-validity-seconds="60"
remember-me-parameter="remember-me"/>
上面的dataSource是我们自己配置的数据库连接池的id
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///security_authority"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
重启项目完成后,再次记住我进行登录后,就可以在数据库中查询到相应的token信息,这时的token信息是保存在数据库中的,就会相对安全一些。
显示当前认证的用户名
在页面中使用下面两个标签都可以显示当前认证的用户名称
<security:authentication property="principal.username"/>
<security:authentication property="name"/>